如何在Multi-Selection RecyclerView中定义选择(热点)区域?

问题描述 投票:0回答:2

我有多项选择

RecyclerView
。我正在尝试处理
RecyclerView
项目内部的按钮单击,当它是
selected/activated
时。但是,当
selected/activated
中的某个项目
RecyclerView
时,我无法单击
ChildView
(按钮),它会处理
RootView
内容并恢复到
deactivated/notselected
状态。

视频


我读了文档,它说

选择热点

这是一个可选功能,用于标识视图中的区域 单击即可选择。通常,当有某个项目时,单击一下即可 任何现有选择都不会导致该项目被激活。如果 点击发生在“选择热点”内,该项目将被 已选择。

有关处理项目激活的详细信息,请参阅OnItemActivatedListener


我尝试

override
在SelectionHotspot功能中

    /**
     * Areas are often included in a view that behave similar to checkboxes, such
     * as the icon to the left of an email message. "selection
     * hotspot" provides a mechanism to identify such regions, and for the
     * library to directly translate taps in these regions into a change
     * in selection state.
     *
     * @return true if the event is in an area of the item that should be
     * directly interpreted as a user wishing to select the item. This
     * is useful for checkboxes and other UI affordances focused on enabling
     * selection.
     */
    public boolean inSelectionHotspot(@NonNull MotionEvent e) {
        return false;
    }

文档的注释参考 Gmail 应用程序,该示例通过单击电子邮件左侧的 ImageView 进行选择,您可以通过单击标题或描述区域来移动电子邮件的内容。这正是我正在尝试做的事情。


ItemDetailsLookup.ItemDetails 类

    class PurchaseItemDetails(var binding: ListObjectLevelPurchaseBinding) : ItemDetailsLookup.ItemDetails<LevelModel>() {
    
        var itemPosition: Int = 0
        lateinit var item: LevelModel
    
    
        override fun getSelectionKey(): LevelModel? {
            return item
        }
    
        override fun getPosition(): Int {
            return itemPosition
        }
    
        override fun inSelectionHotspot(e: MotionEvent): Boolean {
            val locationOnScreen = IntArray(2)
            binding.buttonLevelPurchaseInspect.getLocationOnScreen(locationOnScreen)
            val (left) = locationOnScreen
            val right = left + binding.buttonLevelPurchaseInspect.width
            Log.e("InSelectionHotspot","${e.x.roundToInt() !in left..right}")
            return e.x.roundToInt() !in left..right
        }
    
    }

我也尝试过

override
onItemActivatedListener
没有任何改变。

        tracker = SelectionTracker.Builder<LevelModel>(
            "selection.levels",
            binding.recyclerviewBundleDetailPurchaseLevels,
            PurchaseItemKeyProvider(adapter.currentList),
            PurchaseItemLookup(binding.recyclerviewBundleDetailPurchaseLevels),
            StorageStrategy.createParcelableStorage(LevelModel::class.java)
        ).withSelectionPredicate(
            SelectionPredicates.createSelectAnything()
        ).build()
android kotlin android-recyclerview selection multipleselection
2个回答
0
投票

虽然这个问题很老了,但我认为总会有 Android 开发者在 RecyclerView 的选择 API 上苦苦挣扎。一如既往,谷歌的图书馆缺乏细节和一些有用的例子。这就是为什么我想为那些一直在为选择热点的工作设置而苦苦挣扎的人分享一个解决方案。抱歉给您带来不便,代码是 Java 语言,因为我还没有使用 Kotlin。但我认为将其转换为 Kotlin 很容易。

ItemDetailsLookup.ItemDetails 类

public static class PurchaseItemDetails extends ItemDetailsLookup.ItemDetails<Long> {
    private long position;
    private Rect hotSpot;

    public PurchaseItemDetails() {}

    @Override
    public int getPosition() {
        return (int) position;
    }

    @Nullable
    @Override
    public Long getSelectionKey() {
        return position;
    }

    @Override
    public boolean inSelectionHotspot(@NonNull MotionEvent e) {
        if(hotSpot == null) {
            Log.d(TAG, "inSelectionHotspot: null; Event X "+e.getX()+", Y "+e.getY());
            return false;
        }
        // Here the coordinates of selection hotspot rect can be observed
        Log.d(TAG, String.format("inSelectionHotspot: Hotspot %s; event X %f, Y " +
                "%f", hotSpot.toShortString(), e.getX(), e.getY()));
        return hotSpot.contains((int) e.getX(), (int) e.getY());
    }

    void setPosition(long position) {
        this.position = position;
    }

    void setHotSpot(Rect hotSpot) {
        this.hotSpot = hotSpot;
    }
}

现在我们已经实现了

ItemDetails
类,我们必须在
ViewHolder
类中包含该类,以便能够定义和设置选择热点。

SampleViewHolder 类

class SampleViewHolder extends RecyclerView.ViewHolder {

    private final PurchaseItemDetails details;
    //...

    public SampleViewHolder(@NonNull View itemView) {
        super(itemView);
        details = new PurchaseItemDetails();
    }

    void bind() {
        // Setup selection hotspot
        Rect rect = new Rect();
        // Attention here! We must get the rectangle after the viewholder object has created.
        // Otherwise the getGlobalVisibleRect() will return unexpected values.
        binding.buttonLevelPurchaseInspect.getGlobalVisibleRect(rect);
        // Set the rectangle spot so that the Item details can check and recognize the spot.
        details.setHotSpot(rect);
        // Setup position
        details.setPosition(getBindingAdapterPosition());
        //...
    }
    
    //...
}

最后在适配器类中我们绑定视图持有者的某个地方:

@Override
public void onBindViewHolder(@NonNull SampleViewHolder holder, int position) {
    holder.bind();
}

警告!! 这根本不是完整的代码。实施者必须使其适应他们的代码和便利性。


0
投票

经过一些研究和调试,我发现 getGlobalVisibleRect 可能很有帮助,尽管它根据其父容器返回 rect。 所以这里的技巧是你可以先获取父容器的矩形。 在我的例子中,回收器视图宽度与父级占用设备的整个宽度相匹配,但由于应用程序栏和回收器视图顶部的其他视图,高度是一个问题。

对我有用的技巧是获取回收器视图项根容器的顶部偏移量,然后从实际的热点视图矩形中添加然后减去该偏移量。

这是扩展 ItemDetailsLookup.ItemDetails 的类的代码

public class CustomerSelectionDetail : ItemDetailsLookup.ItemDetails<Long>() {

private var _position: Int = 0
private var _selectionKey: Long = 0L
private var _hotSpot: Rect? = null

override fun getPosition(): Int {
    return _position
}

override fun getSelectionKey(): Long {
    return _selectionKey
}

override fun inSelectionHotspot(e: MotionEvent): Boolean {
    if (_hotSpot == null) {
        Log.d("selection", ("inSelectionHotspot: null; Event X " + e.x).toString() + ", Y " + e.y)
        return false
    }

    Log.d(
        "selection", String.format(
            "inSelectionHotspot: Hotspot %s; event X %f, Y " +
                    "%f", "${_hotSpot!!.top} ${_hotSpot!!.bottom} ${_hotSpot!!.left} ${_hotSpot!!.right}", e.x, e.y
        )
    )

    return _hotSpot!!.contains(e.x.toInt(), e.y.toInt())
}

fun setPosition(position: Int) {
    this._position = position
}

fun setSelectionKey(key: Long) {
    this._selectionKey = key
}

fun setHotSpot(hotSpot: Rect) {
    this._hotSpot = hotSpot
}} 

适配器代码

public class CustomerAdapter(private val context: Context,
                  private val onItemClicked: (AppDefaultActions, Int) -> Unit) : ListAdapter<Customer, RecyclerView.ViewHolder>(DiffCallback())
{
var topOffset = 0
var tracker: SelectionTracker<Long>? = null

inner class ViewHolder(private val binder: SingleCustomerListLayoutBinding) : RecyclerView.ViewHolder(binder.root) {

    val selectionDetails = CustomerSelectionDetail()

    fun bind(item: Customer, isActivated: Boolean = false) {

        selectionDetails.position = adapterPosition
        selectionDetails.selectionKey = getItemId(adapterPosition)

        if (topOffset > 0) {
            setHotspot()
        } else {
            binder.layoutRoot.post {
                val rectLayoutRoot = Rect(0, 0, 0, 0)
                if (binder.layoutRoot.getGlobalVisibleRect(rectLayoutRoot)) {
                    if (topOffset == 0)
                        topOffset = rectLayoutRoot.top

                    setHotspot()
                }
            }
        }


        binder.lblCustomerName.text = item.name
        binder.lblCustomerMobile.text = AppConstants.formatMobileNumber(item.mobileNo)
        binder.lblCustomerAddress.text = "${item.areaName}: ${item.addressDetail}"

        binder.root.isActivated = isActivated
    }

    private fun setHotspot() {
        binder.containerActions.post {
            val hotspot = Rect(0, 0, 0, 0)
            if (binder.containerActions.getGlobalVisibleRect(hotspot)) {
                selectionDetails.setHotSpot(
                    Rect(
                        hotspot.left,
                        hotspot.top - topOffset,
                        hotspot.right,
                        hotspot.bottom - topOffset
                    )
                )
            }
        }
    }
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    val binder = SingleCustomerListLayoutBinding
        .inflate(LayoutInflater.from(parent.context), parent, false)

    return ViewHolder(binder)
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
    tracker?.let {
        if (holder is ViewHolder) {
            val item = getItem(position)
            holder.bind(item, it.isSelected(item.id.toLong()))
        }
    }
}

override fun getItemId(position: Int): Long {
    return getItem(position).id.toLong()
}

init {
    setHasStableIds(true)
}

class DiffCallback : DiffUtil.ItemCallback<Customer>() {
    override fun areItemsTheSame(
        oldItem: Customer,
        newItem: Customer
    ): Boolean {
        return oldItem.id == newItem.id
    }

    override fun areContentsTheSame(
        oldItem: Customer,
        newItem: Customer
    ): Boolean {
        return oldItem == newItem
    }
}
} 

setHotspot() 方法是找到您打算在 Tap 上处理选择的视图的矩形。该方法从顶部和底部减去topOffset。为了找到topOffset,我在内部类ViewHolder的bind方法中添加了逻辑。第一个项目的layoutRoot的topOffset就足够了,因为ItemDetailsLookup.ItemDetails的inSelectionHotspot()重写方法是返回相对于它自己的容器的x和y值。

希望您能够处理您的热点水龙头:-)

© www.soinside.com 2019 - 2024. All rights reserved.