横向wheel效果研发

之前需要实现一个效果,横向滑动,中间是被选中状态,item更大,其他item小,左右滑动时,动态变大变小,且每个item间距一样。

  开发非常痛苦,想了很多方案,网上也没找到合适的。经过艰苦粪斗粪发涂墙,终于勉强弄出来了。只能说,有时候,人还是不要指望依赖外部,自己才最可靠。


先分享下我的有道笔记:https://note.youdao.com/s/KnYcmg4t


这里不再诉苦了,分享干货。

 使用RecyclerView.LayoutManager 来布局

class MyLayoutManager: RecyclerView.LayoutManager

之后布局,要使item能左右滑动,类似wheel ,第一个和最后一个可滑动到中间。需要重写LayoutManager的方法

override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) {

    // 获取重新布局前,滑动的位置,用于刷新后位置与之前一致。

    val lastPosition =  if (childCount == 0) Int.MAX_VALUE else getDecoratedLeft(getChildAt(0)!!)

    // 回收旧的childItem

    recycler?.also { removeAndRecycleAllViews(it) }

    // 如果当前没有childItem,中断

    if (state == null || state.itemCount == 0) {

        return

    }

    // 启动布局,我们假设每个item尺寸都一样

    recycler?.also {

        var length = 0 // 展示完后childItem总的宽度

        var measureStart = 0 // 第一个childItem居中时,滑动的位置

        var measureEnd = 0 // 末位childItem居中时,滑动的位置

        // 计算位置和尺寸

        for (i in 0 until itemCount) {

            val view = recycler.getViewForPosition(i) // 获取itemChild

            addView(view) // 添加到RecyclerView

            measureChildWithMargins(view, 0, 0) // 核算尺寸

            // 获取childItem宽度和ItemDecoration尺寸的总宽度

            val vWidth = getDecoratedMeasuredWidth(view)

            length += vWidth  // 尺寸叠加

            if (i == 0) measureStart = (width - view.width) / 2

            if (i == itemCount - 1) measureEnd = (width + view.width) / 2 - length

        }

        // 根据上次展示位置,计算当前展示childItem的坐标

        var offsetX =

            if (lastPosition > measureStart) measureStart

            else if (lastPosition < measureEnd) measureEnd

            else lastPosition

        for (i in 0 until itemCount) {

            val view = getChildAt(i)!!

            val vWidth = getDecoratedMeasuredWidth(view)

            val vHeight = getDecoratedMeasuredHeight(view)

            layoutDecorated(view, offsetX, (height - vHeight).shr(1),

                        vWidth + offsetX, (height + vHeight).shr(1))

            offsetX += vWidth

            // 此处是自定义方法,根据childView的位置,修改其展示方式

            setChildScaleAndTranslate(view)

        }

    }

}

需要实现左右滑动,需要重写如下方法:

override fun canScrollHorizontally(): Boolean {

    return true

}


其次,需要重写指定方法,实现itemChild的左右移动

override fun scrollHorizontallyBy(dx: Int,

                                recycler: RecyclerView.Recycler?,

                                state: RecyclerView.State?): Int {

    if (itemCount == 0) {

            offsetChildrenHorizontal(0)

            return 0

        }

        var move = -dx // 此次滑动距离

        var result = dx

        val center = width.shr(1) // 组件X轴中间位置坐标

        if (recycler != null) {

            if (dx < 0) { // 向左滑动

                val first = getChildAt(0)!!

                val vCenter = first.let { it.right + it.left shr 1 }

                if (vCenter - dx > center) {

                    move = center - vCenter

                    result = 0

                }

            } else if (dx > 0) { // 向右滑动

                val last = getChildAt(itemCount - 1)!!

                val vCenter = last.let { it.right + it.left shr 1 }

                if (vCenter - dx < center) {

                    move = center - vCenter

                    result = 0

                }

            }

        }

        // 调用父类方法实现具体滑动

        offsetChildrenHorizontal(move)

        for (i in 0 until childCount) {

            val it = getChildAt(i)!!

            // 此处是自定义方法,根据childView的位置,修改其展示方式

            setChildScaleAndTranslate(it, i == 1)

        }

        isScrollingByUser = state != null // 用来判断是否是用户手动操作

        return result

}

具体偏差和缩放展示计算

scaleRate是个常量,0~1之间(不含),缩放后的图片的比率

/**

* 根据view的位置去修改其scale和translate属性

* @param it childView对象

*/

private fun setChildScaleAndTranslate(it: View) {

    val center = width.shr(1)

    val tWidth = it.width + getLeftDecorationWidth(it) +

        getRightDecorationWidth(it)

    val distance = (getDecoratedRight(it)

        + getDecoratedLeft(it)).shr(1) - center

    val distanceAbs = abs(distance)

    val rate =

        if (distanceAbs < tWidth)

            (1f - (1 - scaleRate) * (distanceAbs.toFloat() / tWidth))

        else scaleRate

    it.scaleX = rate

    it.scaleY = rate

    val halfOffset = (1f - scaleRate) * tWidth / 2

    val number = distanceAbs / tWidth

    val r = (distanceAbs % tWidth).toFloat() / tWidth

    val translateX =

        if (distance == 0) 0f

        else if (number == 0 && distance > 0)  ((1 - scaleRate) / 2 * it.width - getLeftDecorationWidth(it)) * r

        else if (number == 0)  ((1 - scaleRate) / 2 * it.width  - getRightDecorationWidth(it)) * r

        else if (distance > 0) (distanceAbs.toFloat() / tWidth * 2 - 1) * halfOffset - number * getLeftDecorationWidth(it) * r

        else (distanceAbs.toFloat() / tWidth * 2 - 1) * halfOffset - number * getRightDecorationWidth(it) - getLeftDecorationWidth(it) * r

it.translationX = if (distance > 0) -translateX else translateX

实现Edge效果,需要重写如下方法

/** 当前滑动偏差,实际返回的最左侧childView.left*/

override fun computeHorizontalScrollOffset(state: RecyclerView.State):Int {if (childCount == 0) return 0for (i in 0 until childCount)

getChildAt(i)?.also {

  if (it.left > 0) return it.left

}

return super.computeHorizontalScrollOffset(state)}

/** 设置可视的范围*/

override fun computeHorizontalScrollExtent(state: RecyclerView.State):Int {

return if (childCount == 0) return 0 else width

}

/**组件可滑动范围, 没有childView时为0,否则,是最右侧childView.right - 最左侧childView.left */

override fun computeHorizontalScrollRange(state: RecyclerView.State): Int {

    return if (childCount == 0) 0

        else {

            val startChild = getChildAt(0)!!

            val endChild = getChildAt(childCount - 1)!!

            endChild.right - startChild.left

        }

}

RecyclerView被清理时,记得回收childView

override fun onDetachedFromWindow(view: RecyclerView?,  recycler: RecyclerView.Recycler?) {

    recycler?.also { removeAndRecycleAllViews(it) }

}

要重写默认布局参数

override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams {

    return RecyclerView.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT,  ViewGroup.LayoutParams.WRAP_CONTENT)

}

要重写scrollToPosition等方法,确保外部调用能准确滚动到指定item上

override fun scrollToPosition(position: Int) {

    if (position < 0 || itemCount == 0 || position >= itemCount) return

    val v = getChildAt(position) ?: return

    val move = (v.right + v.left).shr(1) - width.shr(1)

    selectListener?.onSelect(position, false)

    for (i in 0 until itemCount) {

        if (i == position) {

            v.isSelected = true

            continue

        }

        getChildAt(i)?.also { if (it.isSelected) it.isSelected = false }

    }

    smoothAnimBy(move)

}    

override fun smoothScrollToPosition(recyclerView: RecyclerView?

                , state: RecyclerView.State?, position: Int) {

    if (position < 0 || itemCount == 0 || position >= itemCount)

        return

    val v = getChildAt(position) ?: return

    val move = (v.right + v.left).shr(1) - width.shr(1)

    selectListener?.onSelect(position, false)

    for (i in 0 until itemCount) {

        if (i == position) {

            v.isSelected = true

            continue

        }

        getChildAt(i)?.also { if (it.isSelected) it.isSelected = false }

    }

    smoothAnimBy(move, 100L * (

        abs(move) / (getDecoratedRight(v) + getDecoratedLeft(v)) + 1))

}

剩下滑动停止后,item自动滚动到中间对齐。

需要计算距离和动画,结合ValueAnimator实现, 并且滑动后选中监听反馈。

重写onScrollStateChanged方法,监听列表滚动停止时(RecyclerView.SCROLL_STATE_IDLE),计算需要动画滚动多少距离。

// 定义一个动画对象

private var smoothScroller: ValueAnimator? = null

// 加速器

private val mInterpolator = DecelerateInterpolator()

// 用于保存动画偏差的全局变量

private var lastAnimValue: Int = 0

// 动画监听回调

private val smoothScrollerListener by lazy {

    ValueAnimator.AnimatorUpdateListener { animation ->

        animation?.also {

            val value = (it.animatedValue as Int)

            scrollHorizontallyBy(value - lastAnimValue, null, null)

            lastAnimValue = value

        }

    }

}

/**

* 动画滑动

* @param end 偏移量

* @param duration 动画时间,默认200ms

*/

private fun smoothAnimBy(end: Int, duration: Long = 200) {

    smoothScroller?.also {

        it.removeUpdateListener(smoothScrollerListener)

        it.cancel()

    }

    lastAnimValue = 0

    if (end == 0) return

    smoothScroller = ValueAnimator.ofInt(0, end).apply {

        this.duration = duration

        interpolator = mInterpolator

        addUpdateListener(smoothScrollerListener)

        start()

    }

}

// 监听回调对象

var selectListener: MySelectListener? = null

// 回调接口

interface MySelectListener {

    fun onSelect(position: Int, isSelectByUser: Boolean = false)

}

// 重写方法,接收滚动时,启动动画滚动到正确位置

override fun onScrollStateChanged(state: Int) {

    if (state == RecyclerView.SCROLL_STATE_IDLE) {

        val center = width.shr(1)

        for (i in 0 until itemCount) {

            val v = getChildAt(i)!!

            val marginLeft = getLeftDecorationWidth(v)

            val marginRight = getRightDecorationWidth(v)

            v.isSelected = center <= v.right + marginRight

                      && center >= v.left - marginLeft // 选中状态

            if (v.isSelected) {

                val move = (v.right + v.left).shr(1) - center

                selectListener?.onSelect(i, isScrollingByUser)

                isScrollingByUser = false

                smoothAnimBy(move)

            }

        }

    }

}

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容