Skip to content
Beka Westberg edited this page Dec 23, 2019 · 2 revisions

A ViewPicker is a function used to choose a visible view associated with a given adapter index, when there could me more than one view associated with the given adapter index.

ViewPickers are passed to functions like findViewByPosition.

Built-in ViewPickers

This library provides two built-in ViewPickers childClosestToMiddle and childClosestToAnchorEdge.

childClosestToMiddle

The childClosestToMiddle ViewPicker is the default ViewPicker. If you do not explicitly pass a ViewPicker to a function that requires one, this is the picker that will be used.

It returns the view (associated with the given adapter index) whose middle is closest to the middle of the layout.

[TODO: Add image of picking a view. #21]

childClosestToAnchorEdge

This ViewPicker returns the view (associated with the given adapter index) that is closest to the "anchor edge" of the layout.

The "anchor edge" is the edge where the view with the 0 adapter index was initially laid out. For example, in a default vertical layout, the anchor edge would be the top. In a reversed vertical layout, the anchor edge would be the bottom.

[TODO: Add image of picking a view. #21]

Creating a custom view picker

Sometimes these built-in functions don't cover the needs of your app. Luckily! You can write a custom view picker for those occations.

Signature

A ViewPicker has the following function signature:

(targetAdapterIndex: Int, layoutManager: LoopingLayoutManager) -> View?

It takes in a target adapter index (Int) and the layout manager (LoopingLayoutManager) it should be searching for the view within. Then it returns a view associated with the given adapter index, or null if one could not be found.

Basics

How you want to write your view picker is highly dependent on what you want to achieve, but there are a few recommendations that should work in most cases.

Use a loop

It may be tempting to try and find the correct view using complex math (we've all been there). But in most cases, it's not that advantageous compared to simply looping. A loop won't be horribly inefficient because it won't be going over many views (that's the whole point of a recycler!). And in most cases to properly do math you'd need to access the length of loop. Sadly we don't have access to the state.itemCount, so you'd have to resort to using the adapter count, which could be inaccurate.

Useful functions

  • layoutManager.getChildAt(index) returns the child at the specified child index.

    Note: It does not return the child at an adapter index, if it did we wouldn't need to be creating a view picker!

  • layoutManager.getPosition(view) returns the adapter index the given view is associated with.

  • layoutManager.convertAdapterDirToMovementDir(direction) converts an adapter direction (TOWARDS_HIGHER_INDICES or TOWARDS_LOWER_INDICES) to a movement direction (TOWARDS_TOP_LEFT or TOWARDS_BOTTOM_RIGHT). This can be useful when determining which direction to loop in.

    See directional conversion for more information.

Example: childClosestToMiddle

Example is written in kotlin, with code comments explaining what is happening.

fun childClosestToMiddle(
        targetAdapterIndex: Int,
        layoutManager: LoopingLayoutManager
): View? {
    // Set up our tracking variables. We want to find the lowest distance,
    // so we start with a really large one.
    var minDistance = Int.MAX_VALUE
    var closestView: View? = null

    // Find the middle of the layout manager, depending on if it is horizontal or vertical.
    val layoutMiddle = if (layoutManager.orientation == LoopingLayoutManager.HORIZONTAL) {
        layoutManager.paddingLeft + (layoutManager.width / 2)
    } else {
        layoutManager.paddingTop + (layoutManager.height / 2)
    }

    // Loop through the views owned by the layout manager.
    for (i in 0 until layoutManager.childCount) {
        // Get the view.
        val view = layoutManager.getChildAt(i) ?: return null
        // Check if the adapter position matches. If it does not, skip.
        if (layoutManager.getPosition(view) != targetAdapterIndex) {
            continue
        }
        // If it does, calculate the middle of the view.
        val childMiddle = if (layoutManager.orientation == LoopingLayoutManager.HORIZONTAL) {
            layoutManager.getDecoratedLeft(view) +
                    (layoutManager.getDecoratedMeasuredWidth(view) / 2)
        } else {
            layoutManager.getDecoratedTop(view) +
                    (layoutManager.getDecoratedMeasuredHeight(view) / 2)
        }
        // Find the distance between the middle of the view and the middle of the layout.
        val distance = abs(childMiddle - layoutMiddle)
        // If the distance is less the the current shortest distance, we know that
        // this view is closer to the middle.
        if (distance < minDistance) {
            minDistance = distance
            closestView = view
        }
    }
    // Return the view with the given adapter index that is closest to the middle.
    // Or null if we didn't find any views with the given adapter index.
    return closestView
}