横向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)

            }

        }

    }

}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,755评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,305评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,138评论 0 355
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,791评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,794评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,631评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,362评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,264评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,724评论 1 315
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,900评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,040评论 1 350
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,742评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,364评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,944评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,060评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,247评论 3 371
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,979评论 2 355

推荐阅读更多精彩内容