RecyclerView是android提供的替代ListView的方法,与ListView不同的是,它抽象层次更好,把列表视图抽象成可回收视图,RecyclerView负责了核心的缓存与重用部分的功能。layout的东西交给了layoutManager去处理,以LinearLayoutManager为例子,它负责了子view的位置摆放(onLayoutChildren),ItemTouch事件的处理,这几个东西抽象出来之后,一些自定义的东西就比较好控制了。 这次交互提出了一些功能,需要使用到RecyclerView的扩展使用,摸索了一下,这篇文章当作是前几天摸索的总结吧。
1,像探探一样,三四个Item叠在一起,Item可拖拽,拖拽过半屏,松手就remove掉它 2,下一个至于顶部的item就开始播放其中的视屏
核心代码就是 1,重写RecyclerView.LayoutManager中的onLayoutChildren函数,设置了初始的位置 2,重写ItemTouchHelper.Callback onChildDraw,动态缩放拖拽时的伸缩量,修改View.scaleX,View.scaleY,伸缩底层的Item,增加Item的时候,就notifyItemInsert,可以避免全量刷新引起的动荡。 3,重写ItemTouchHelper.Callback getMovementFlags
override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { if (viewHolder.layoutPosition == 0) {//只允许最定的Item可以拖拽 val dragFlags = 0 val swipeFlags = ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT or ItemTouchHelper.UP or ItemTouchHelper.DOWN return ItemTouchHelper.Callback.makeMovementFlags(dragFlags, swipeFlags) } else {//不在顶部的Item不相应拖拽事件 return ItemTouchHelper.Callback.makeMovementFlags(0, 0) } }ItemTouchHelper.LEFT ItemTouchHelper.RIGHT …分别表示允许左拖拽 右拖拽 上拖拽 下拖拽 4,还有补一个onTouch的回掉
private val mTopTouchListener: OnTouchListener = View.OnTouchListener { v, event -> val childViewHolder = mRecyclerView.getChildViewHolder(v) val action = MotionEventCompat.getActionMasked(event) if (action == MotionEvent.ACTION_DOWN) { mTouchHelper?.startSwipe(childViewHolder) } false }5,置顶事件需要重载 ItemTouchHelper.Callback onSwiped
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { viewHolder.itemView.setOnTouchListener(null)//移除 onTouchListener,否则触摸滑动会乱了 mAdapter?.apply { val layoutPosition = viewHolder.layoutPosition swiped(viewHolder, layoutPosition, direction) val topIndex = mRecyclerView.childCount - 2 swipeTop(if (topIndex < 0) null else mRecyclerView.getChildViewHolder(mRecyclerView.getChildAt(topIndex))) //数据源不足时,提前加载 if (itemCount < DEFAULT_SHOW_ITEM * 2) { load(currentTask.get()) } } }大概就是这样,在RecyclerView高度模块化时候,ItemTouchHelper 处理了大部分的手势信息,所以实际代码反而比预期少了很多。
后来交互改了。说实话,我不反感改交互,但是前提是要预留时间让我做。 程序员痛恨改交互的原因应该是deadline不变,然后改了一大波交互 : (
所以有
1,横向的循环列表(拉倒的第一波数据的权重最高,越往后拉权重越低) 2,滑动后松手,Item会自动居中 3,居中的视屏开始播放,停止掉上一个播放中的视屏
核心代码就是: 1,循环列表要在数据结构中处理,如下面代码,简单说就是初始化的时候滑动到 Int.maxValue/2 ,然后修改从position映射数据到ViewHolder的方法,例如:
//去重复、循环列表控件 class LoopList<T> { private val data = LinkedHashSet<T>() fun baseLine() = size() / 2 fun size(): Int = if (data.isEmpty()) 0 else Int.MAX_VALUE fun realSize(): Int = data.size /** * * 把列表映射成循环列表 * baseline * | #2 | #1 | #3 | #4 | * 数据: 0 1 3 5 7 9 8 6 4 2 0 1 3 5 7 9 8 6 4 2 0 1 * 视图:-10 -9 ... -1 0 1 2 3 4 5 6 7 8 9 10 11 * */ fun get(index: Int): T? { val length = data.size if (length == 0) return null val pos = (index - baseLine()) % length val mapping = when { (pos > 0 && pos > length / 2) -> -(pos - length) * 2 //#4 (pos > 0) -> pos * 2 - 1 //#3 (-pos >= length - length / 2) -> (pos + length) * 2 - 1 //#2 else -> -pos * 2 //#1 } //TODO ensure mapping in range of data Lg.d("LoopLis >> $pos $mapping") return data.elementAt(mapping) } fun add(item: T) { data.add(item) } fun addAll(array: List<T>) { data.addAll(array) } fun clear() { data.clear() } fun isEmpty(): Boolean { return data.isEmpty() } fun isExceeding(index: Int, offset: Int = 0): Boolean { if (data.size == 0) return true return data.size - Math.abs(index - baseLine()) % data.size < offset } }映射方法很多拉,看具体业务怎么定,需要注意的是 A,要处理没有数据的特殊情况,不要给映射,要如实返回长度是0 B,滑动过去的时候,mRecyclerView可能不能很好的卡住位,第一个scroll的位置也可以算错,所以最后一个要使用smoothScrollToPosition
mRecyclerView.scrollToPosition(mAdapter!!.baseLine() - 1) mCardScaleHelper.mBaseLine = mAdapter!!.baseLine() - 1 mRecyclerView.post { //triggering snap helper to attach the item mRecyclerView.smoothScrollToPosition(mAdapter!!.baseLine()) }2,自动居中 android有提供了一个LinearSnapHelper, e.g.:mLinearSnapHelper.attachToRecyclerView(recyclerView)
东西都算好了,就居中了。 3,居中动画结束之后要给回掉,播放视频,我是重载了LinearSnapHelper calculateDistanceToFinalSnap。 LinearSnapHelper的基本思路是,在松手的时候算距离是不是居中,如果是居中的话,就不用动,不是居中的话,就滑倒中间去。calculateDistanceToFinalSnap就是计算需不需要滑动过去的,(calculateDistanceToFinalSnap是在onScrollStateChanged newState == RecyclerView.SCROLL_STATE_IDLE 的回掉中调用的。)这里只需要判断这个时候是不是不需要动画滚动,如果是的话,那么就置顶了。
inner class CardLinearSnapHelper : LinearSnapHelper() { var lastPosition = 0 var mNoNeedToScroll = false override fun calculateDistanceToFinalSnap(layoutManager: RecyclerView.LayoutManager, targetView: View): IntArray? { val calculate = if (mNoNeedToScroll) intArrayOf(0, 0) else super.calculateDistanceToFinalSnap(layoutManager, targetView) if (calculate != null && !calculate.isEmpty()) { if (calculate[0] == 0) {//distanceToCenter == 0 不需要重新定位Y轴位置 mRecyclerView?.getChildLayoutPosition(targetView)?.apply { //找到停留的位置 if (this >= 0 && lastPosition != this) { lastPosition = this mPositionChangeListener?.onCurrent(this) } } } } return calculate } }PS:代码是用kotlin写的,可能有些同学用的是java,直接copy也没什么用,权当作参考吧:)
交互一:
class PagedCardRecyclerLayout : FrameLayout { val mRecyclerView: RecyclerView by bind(R.id.view_paged_card_recycler_list, comment = "列表视图") private val mLoading: LoadingView by bind(R.id.view_paged_card_recycler_loading, comment = "加载视图") private val currentTask = AtomicReference<PagedRecyclerLayout.DataSetter>() private var mAdapter: CardAdapter<*>? = null private var mDataSupport: PagedRecyclerLayout.DataSupporter? = null private var mTouchHelper: ItemTouchHelper? = null private val mTopTouchListener: OnTouchListener = View.OnTouchListener { v, event -> val childViewHolder = mRecyclerView.getChildViewHolder(v) val action = MotionEventCompat.getActionMasked(event) if (action == MotionEvent.ACTION_DOWN) { mTouchHelper?.startSwipe(childViewHolder) } false } constructor(context: Context?) : this(context, null) constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { View.inflate(context, R.layout.view_paged_card_recycler_layout, this) mRecyclerView.itemAnimator = DefaultItemAnimator() mRecyclerView.layoutManager = CardLayoutManager().apply { topTouchListener = mTopTouchListener } } fun <T : Any> attachData(adapter: CardAdapter<T>, support: PagedRecyclerLayout.DataSupporter) { setDataSupport(support) setAdapter(adapter) } fun setDataSupport(support: PagedRecyclerLayout.DataSupporter) { mDataSupport = support } fun <T : Any> setAdapter(adapter: CardAdapter<T>) { mAdapter = adapter mTouchHelper = ItemTouchHelper(CardItemTouchHelperCallback()) mTouchHelper?.attachToRecyclerView(mRecyclerView) mRecyclerView.adapter = adapter adapter.onPrepared(mRecyclerView) reload() } fun notifyDataSetChanged() { mAdapter?.apply { notifyDataSetChanged() if (data.isEmpty()) { mLoading.empty(emptyIcon(), emptyHint()) } else { mLoading.success() } } } fun reload() { load(CardDataSetter(page = 1, offset = 0)) } fun load(setter: PagedRecyclerLayout.DataSetter) { if (setter.isLoading) return currentTask.set(setter) setter.isLoading = true mDataSupport?.onLoad(setter) } inner class CardDataSetter(page: Int, offset: Int) : PagedRecyclerLayout.DataSetter(page, offset) { override fun success(data: List<Any>, offset: Int, hasMore: Boolean) { val setter = currentTask.get() if (setter == null || setter != this) return mAdapter?.hasMore = hasMore if (setter.isFirstLoad()) mAdapter?.set(data) else mAdapter?.append(data) if (setter.isFirstLoad() && data.isEmpty() && mAdapter != null) { mLoading.empty(mAdapter!!.emptyIcon(), mAdapter!!.emptyHint()) } else { mLoading.success() } if (!hasMore && mAdapter != null && mAdapter!!.loopLoading()) { currentTask.set(CardDataSetter(page = 1, offset = 0)) return } else { currentTask.set(CardDataSetter(setter.page + 1, offset)) } post { val topIndex = mRecyclerView.childCount - 1 mAdapter?.swipeTop(if (topIndex < 0) null else mRecyclerView.getChildViewHolder(mRecyclerView.getChildAt(topIndex))) } } override fun fail(message: String) { Toasts.str(message) val setter = currentTask.get() if (setter == null || setter != this) return setter.isLoading = false mLoading.fail(message, { mLoading.load() reload() }) mAdapter?.notifyDataSetChanged() } } /** * 根据滑动或者拖拽,伸缩底层子控件以及事件回调 */ inner class CardItemTouchHelperCallback : ItemTouchHelper.Callback() { override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int { if (viewHolder.layoutPosition == 0) { val dragFlags = 0 val swipeFlags = ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT or ItemTouchHelper.UP or ItemTouchHelper.DOWN return ItemTouchHelper.Callback.makeMovementFlags(dragFlags, swipeFlags) } else { return ItemTouchHelper.Callback.makeMovementFlags(0, 0) } } override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean { return false } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { viewHolder.itemView.setOnTouchListener(null)//移除 onTouchListener,否则触摸滑动会乱了 mAdapter?.apply { val layoutPosition = viewHolder.layoutPosition swiped(viewHolder, layoutPosition, direction) val topIndex = mRecyclerView.childCount - 2 swipeTop(if (topIndex < 0) null else mRecyclerView.getChildViewHolder(mRecyclerView.getChildAt(topIndex))) //数据源不足时,提前加载 if (itemCount < DEFAULT_SHOW_ITEM * 2) { load(currentTask.get()) } } } override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) { super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive) val itemView = viewHolder.itemView //根据滑动距离算子控件伸缩大小 if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) { val threshold = recyclerView.width * getSwipeThreshold(viewHolder) * 2 var elasticity = (dX + dY) / if (threshold == 0F) 1F else threshold elasticity = if (elasticity > 1) 1F else if (elasticity < -1) -1F else elasticity val childCount = recyclerView.childCount if (childCount > DEFAULT_SHOW_ITEM) { for (position in 1..childCount - 1 - 1) { val index = childCount - position - 1 val scale = 1 - index * DEFAULT_SCALE + Math.abs(elasticity) * DEFAULT_SCALE val translation = (index - Math.abs(elasticity)) * itemView.measuredWidth / DEFAULT_TRANSLATE_Y recyclerView.getChildAt(position).apply { scaleX = scale scaleY = scale translationX = translation setOnTouchListener(null) } } } else { for (position in 0..childCount - 1 - 1) { val index = childCount - position - 1 val scale = 1 - index * DEFAULT_SCALE + Math.abs(elasticity) * DEFAULT_SCALE val translation = (index - Math.abs(elasticity)) * itemView.measuredWidth / DEFAULT_TRANSLATE_Y recyclerView.getChildAt(position).apply { scaleX = scale scaleY = scale translationX = translation setOnTouchListener(null) } } } mAdapter?.onSwiping(viewHolder, elasticity, when { elasticity == 0f -> SWIPING_NONE elasticity < 0 -> SWIPING_LEFT else -> SWIPING_RIGHT }) } } override fun clearView(recyclerView: RecyclerView?, viewHolder: RecyclerView.ViewHolder) { super.clearView(recyclerView, viewHolder) } } abstract class CardAdapter<T> : SwipeMenuAdapter<RecyclerView.ViewHolder>() , PagedCardRecyclerLayout.OnCardSwipeListener<T> { var data: ArrayList<T> = ArrayList() var hasMore: Boolean = false fun emptyHint(): String = "这里空空如也" fun emptyIcon(): Int = R.drawable.default_none_icon override fun getItemCount(): Int { return data.size } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { if (position in 0..(data.size - 1)) { onBindViewHolder(holder, data[position], position) } else { onBindViewHolder(holder, null, position) } } fun isEmpty(): Boolean = data.isEmpty() @Suppress("UNCHECKED_CAST") fun set(dataArray: List<Any>?) { (dataArray as? List<T>)?.apply { data.clear() data.addAll(this) notifyDataSetChanged() } } @Suppress("UNCHECKED_CAST") fun append(dataArray: List<Any>?) { (dataArray as? List<T>)?.apply { data.addAll(this) notifyDataSetChanged() } } fun swiped(viewHolder: RecyclerView.ViewHolder, layoutPosition: Int, direction: Int) { val remove = data.removeAt(layoutPosition) notifyItemRemoved(layoutPosition) onSwiped(viewHolder, remove, if (direction == ItemTouchHelper.LEFT || direction == ItemTouchHelper.UP) SWIPED_LEFT else SWIPED_RIGHT) } fun swipeTop(viewHolder: RecyclerView.ViewHolder?) { val top = data.firstOrNull() onSwipeTop(viewHolder, top) } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, t: T, direction: Int) {} override fun onSwiping(viewHolder: RecyclerView.ViewHolder, ratio: Float, direction: Int) {} override fun onSwipeTop(viewHolder: RecyclerView.ViewHolder?, t: T?) {} open fun onPrepared(list: RecyclerView) {} open fun loopLoading(): Boolean = false abstract fun onBindViewHolder(holder: RecyclerView.ViewHolder, item: T?, position: Int) } /** * 绘制控件层叠样式 */ class CardLayoutManager : RecyclerView.LayoutManager() { var topTouchListener: OnTouchListener? = null override fun generateDefaultLayoutParams(): RecyclerView.LayoutParams { return RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) } override fun onLayoutChildren(recycler: RecyclerView.Recycler?, state: RecyclerView.State?) { detachAndScrapAttachedViews(recycler) val itemCount = itemCount // 静态、默认子控件大小绘制 if (itemCount > DEFAULT_SHOW_ITEM) { for (position in DEFAULT_SHOW_ITEM downTo 0) { val view = recycler!!.getViewForPosition(position) addView(view) measureChildWithMargins(view, 0, 0) view.setOnTouchListener(null) val widthSpace = width - getDecoratedMeasuredWidth(view) val heightSpace = height - getDecoratedMeasuredHeight(view) layoutDecoratedWithMargins(view, widthSpace / 2, heightSpace / 2, widthSpace / 2 + getDecoratedMeasuredWidth(view), heightSpace / 2 + getDecoratedMeasuredHeight(view)) if (position == DEFAULT_SHOW_ITEM) { view.apply { scaleX = 1 - (position - 1) * DEFAULT_SCALE scaleY = 1 - (position - 1) * DEFAULT_SCALE translationX = (position - 1) * view.measuredWidth / DEFAULT_TRANSLATE_Y } } else if (position > 0) { view.apply { scaleX = 1 - position * DEFAULT_SCALE scaleY = 1 - position * DEFAULT_SCALE translationX = position * view.measuredWidth / DEFAULT_TRANSLATE_Y } } else { view.setOnTouchListener(topTouchListener) } } } else { // 当数据源个数小于或等于最大显示数时 for (position in itemCount - 1 downTo 0) { val view = recycler!!.getViewForPosition(position) addView(view) measureChildWithMargins(view, 0, 0) view.setOnTouchListener(null) val widthSpace = width - getDecoratedMeasuredWidth(view) val heightSpace = height - getDecoratedMeasuredHeight(view) layoutDecoratedWithMargins(view, widthSpace / 2, heightSpace / 2, widthSpace / 2 + getDecoratedMeasuredWidth(view), heightSpace / 2 + getDecoratedMeasuredHeight(view)) if (position > 0) { view.apply { scaleX = 1 - position * DEFAULT_SCALE scaleY = 1 - position * DEFAULT_SCALE translationX = position * view.measuredWidth / DEFAULT_TRANSLATE_Y } } else { view.setOnTouchListener(topTouchListener) } } } } } interface OnCardSwipeListener<in T> { fun onSwiping(viewHolder: RecyclerView.ViewHolder, ratio: Float, direction: Int) fun onSwiped(viewHolder: RecyclerView.ViewHolder, t: T, direction: Int) fun onSwipeTop(viewHolder: RecyclerView.ViewHolder?, t: T?) } companion object { val DEFAULT_SHOW_ITEM = 2 //显示可见的卡片数量 val DEFAULT_SCALE = 0.1f //默认缩放的比例 val DEFAULT_TRANSLATE_Y = 14F //卡片Y轴偏移量时按照14等分计算 val SWIPING_NONE = 1 //卡片滑动时不偏左也不偏右 val SWIPING_LEFT = 1 shl 2 //卡片向左滑动时 val SWIPING_RIGHT = 1 shl 3 //卡片向右滑动时 val SWIPED_LEFT = 1 //卡片从左边滑出 val SWIPED_RIGHT = 1 shl 2 //卡片从右边滑出 } }交互二:
/** * refer to http://www.jianshu.com/p/85bf072bfeed * 循环卡片列表 */ class PagedGalleryLayout : FrameLayout { private val mRecyclerView: SpeedRecyclerView by bind(R.id.view_paged_card_recycler_list, comment = "列表视图") private val mLoading: LoadingView by bind(R.id.view_paged_card_recycler_loading, comment = "加载视图") private val mCurrentTask = AtomicReference<PagedRecyclerLayout.DataSetter>() private var mAdapter: CardAdapter<*>? = null private val mCardScaleHelper = CardScaleHelper() private var mDataSupport: PagedRecyclerLayout.DataSupporter? = null private var mCacheHolder: WeakReference<RecyclerView.ViewHolder>? = null constructor(context: Context?) : this(context, null) constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs, 0) constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { View.inflate(context, R.layout.view_paged_card_recycler_layout, this) mRecyclerView.setItemViewCacheSize(9) mRecyclerView.setScrollingTouchSlop(TOUCH_SLOP_PAGING) mRecyclerView.layoutManager = LinearLayoutManager( context, LinearLayoutManager.HORIZONTAL, false).apply { recycleChildrenOnDetach = true } mCardScaleHelper.apply { attachToRecyclerView(mRecyclerView) mPositionChangeListener = object : CardScaleHelper.OnScalePositionChange { override fun onCurrent(position: Int) { prepareLoadMore(position) watchScroll(position) } fun prepareLoadMore(position: Int) { if (mAdapter == null || !mAdapter!!.hasMore) return (mRecyclerView.layoutManager as? LinearLayoutManager?)?.apply { if (mAdapter!!.isExceeding(position)) load(mCurrentTask.get()) } } } } } private fun load(setter: PagedRecyclerLayout.DataSetter) { Lg.d("load setter ${setter.offset} ${setter.page}") if (setter.isLoading) return mCurrentTask.set(setter) setter.isLoading = true mDataSupport?.onLoad(setter) } private fun watchScroll(position: Int) { Lg.d("watchScroll ${position - Int.MAX_VALUE / 2}") mRecyclerView.findViewHolderForAdapterPosition(position).apply { mAdapter?.onScroll(this, mCacheHolder?.get()) mCacheHolder = WeakReference<RecyclerView.ViewHolder>(this) } } fun <T : Any> attachData(adapter: CardAdapter<T>, support: PagedRecyclerLayout.DataSupporter) { setDataSupport(support) setAdapter(adapter) } fun setDataSupport(support: PagedRecyclerLayout.DataSupporter) { mDataSupport = support } fun <T : Any> setAdapter(adapter: CardAdapter<T>) { mAdapter = adapter mRecyclerView.adapter = adapter adapter.onPrepared(mRecyclerView) reload() } fun notifyDataSetChanged() { mAdapter?.apply { notifyDataSetChanged() if (data.isEmpty()) { mLoading.empty(emptyIcon(), emptyHint()) } else { mLoading.success() } } } fun reload() { load(CardDataSetter(page = 1, offset = 0)) } fun getTopViewHolder(): RecyclerView.ViewHolder? { return mCacheHolder?.get() } inner class CardDataSetter(page: Int, offset: Int) : PagedRecyclerLayout.DataSetter(page, offset) { override fun success(data: List<Any>, offset: Int, hasMore: Boolean) { val setter = mCurrentTask.get() if (setter == null || setter != this || mAdapter == null) return mAdapter!!.hasMore = hasMore if (setter.isFirstLoad()) { mAdapter!!.set(data) mRecyclerView.scrollToPosition(mAdapter!!.baseLine() - 1) mCardScaleHelper.mBaseLine = mAdapter!!.baseLine() - 1 mRecyclerView.post { //triggering snap helper to attach the item mRecyclerView.smoothScrollToPosition(mAdapter!!.baseLine()) } } else { mAdapter!!.append(data) } if (setter.isFirstLoad() && data.isEmpty() && mAdapter != null) { mLoading.empty(mAdapter!!.emptyIcon(), mAdapter!!.emptyHint()) } else { mLoading.success() } if (hasMore) mCurrentTask.set(CardDataSetter(setter.page + 1, offset)) } override fun fail(message: String) { Toasts.str(message) val setter = mCurrentTask.get() if (setter == null || setter != this) return setter.isLoading = false mLoading.fail(message, { mLoading.load() reload() }) mAdapter?.notifyDataSetChanged() } } abstract class CardAdapter<T> : SwipeMenuAdapter<RecyclerView.ViewHolder>() { private val cardAdapterHelper = CardAdapterHelper() var data: LoopList<T> = LoopList() var hasMore: Boolean = false open fun emptyHint(): String = "这里空空如也" open fun emptyIcon(): Int = R.drawable.default_none_icon open fun mixedHead(): Boolean = false fun baseLine(): Int = data.baseLine() override fun getItemCount(): Int { return data.size() } override final fun onCreateContentView(parent: ViewGroup, viewType: Int): View { val contentView = onCreateContent(parent, viewType) cardAdapterHelper.onCreateViewHolder(parent, contentView) return contentView } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { cardAdapterHelper.onBindViewHolder(holder.itemView, position, itemCount) if (position in 0..(data.size() - 1)) { onBindViewHolder(holder, data.get(position), position) } else { onBindViewHolder(holder, null, position) } } fun isEmpty(): Boolean = data.isEmpty() fun isExceeding(index: Int): Boolean { //检查是否到达边界,是否需要加载新数据 return data.isExceeding(index, 5) } @Suppress("UNCHECKED_CAST") fun set(dataArray: List<Any>?) { (dataArray as? List<T>)?.apply { data.clear() if (mixedHead() && !this.isEmpty()) { data.add(this[(Math.random() * (this.size - 1)).toInt()]) } data.addAll(this) notifyDataSetChanged() } } @Suppress("UNCHECKED_CAST") fun append(dataArray: List<Any>?) { val oldSize = data.realSize() (dataArray as? List<T>)?.apply { data.addAll(this) if (oldSize != data.realSize()) { notifyDataSetChanged() } } } open fun onCreateContent(parent: ViewGroup, viewType: Int): View = View(parent.context) open fun onScroll(current: RecyclerView.ViewHolder?, last: RecyclerView.ViewHolder?) {} open fun onPrepared(list: RecyclerView) {} abstract fun onBindViewHolder(holder: RecyclerView.ViewHolder, item: T?, position: Int) } } //去重复、循环列表控件 class LoopList<T> { private val data = LinkedHashSet<T>() fun baseLine() = size() / 2 fun size(): Int = if (data.isEmpty()) 0 else Int.MAX_VALUE fun realSize(): Int = data.size /** * * 把列表映射成循环列表 * baseline * | #2 | #1 | #3 | #4 | * 数据: 0 1 3 5 7 9 8 6 4 2 0 1 3 5 7 9 8 6 4 2 0 1 * 视图:-10 -9 ... -1 0 1 2 3 4 5 6 7 8 9 10 11 * */ fun get(index: Int): T? { val length = data.size if (length == 0) return null val pos = (index - baseLine()) % length val mapping = when { (pos > 0 && pos > length / 2) -> -(pos - length) * 2 //#4 (pos > 0) -> pos * 2 - 1 //#3 (-pos >= length - length / 2) -> (pos + length) * 2 - 1 //#2 else -> -pos * 2 //#1 } //TODO ensure mapping in range of data Lg.d("LoopLis >> $pos $mapping") return data.elementAt(mapping) } fun add(item: T) { data.add(item) } fun addAll(array: List<T>) { data.addAll(array) } fun clear() { data.clear() } fun isEmpty(): Boolean { return data.isEmpty() } fun isExceeding(index: Int, offset: Int = 0): Boolean { if (data.size == 0) return true return data.size - Math.abs(index - baseLine()) % data.size < offset } } //Following code is copy from https://github.com/huazhiyuan2008/RecyclerViewCardGallery //控制RecyclerView的滚动速度 class SpeedRecyclerView : RecyclerView { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) override fun fling(velocityX: Int, velocityY: Int): Boolean { return super.fling(solveVelocity(velocityX), solveVelocity(velocityY)) } private fun solveVelocity(velocity: Int): Int { return if (velocity > 0) Math.min(velocity, FLING_MAX_VELOCITY) else Math.max(velocity, -FLING_MAX_VELOCITY) } companion object { private val FLING_MAX_VELOCITY = 8000 // 最大顺时滑动速度 } } //计算、控制ItemView缩放 class CardScaleHelper { private val mLinearSnapHelper = CardLinearSnapHelper() private var mRecyclerView: RecyclerView? = null private var mCardWidth: Int = 0 // 卡片宽度 private var mOnePageWidth: Int = 0 // 滑动一页的距离 private var mCardGalleryWidth: Int = 0 private var mCurrentItemOffset: Int = 0 private var currentItemPos: Int = 0 var mScale = 0.9f // 两边视图scale var mPagePadding = 15 // 卡片的padding, 卡片间的距离等于2倍的mPagePadding var mShowLeftCardWidth = 15 // 左边卡片显示大小 var mBaseLine = 0 //滑动基准线 var mPositionChangeListener: OnScalePositionChange? = null fun attachToRecyclerView(recyclerView: RecyclerView) { this.mRecyclerView = recyclerView recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrollStateChanged(recyclerView: RecyclerView?, newState: Int) { super.onScrollStateChanged(recyclerView, newState) if (recyclerView == null) return if (newState == RecyclerView.SCROLL_STATE_IDLE) { val destItemOffset = mOnePageWidth * (recyclerView.adapter.itemCount - 1) val noNeed = (mCurrentItemOffset == destItemOffset) or (mCurrentItemOffset == 0) mLinearSnapHelper.mNoNeedToScroll = noNeed } else { mLinearSnapHelper.mNoNeedToScroll = false } } override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) { super.onScrolled(recyclerView, dx, dy) // dx>0则表示右滑, dx<0表示左滑, dy<0表示上滑, dy>0表示下滑 mCurrentItemOffset += dx computeCurrentItemPos() onScrolledChangedCallback() } }) initWidth(recyclerView) mLinearSnapHelper.attachToRecyclerView(recyclerView) } //初始化卡片宽度 private fun initWidth(recyclerView: RecyclerView) { recyclerView.post { mCardGalleryWidth = recyclerView.width mCardWidth = mCardGalleryWidth - DensityUtils.dip2px(2 * (mPagePadding + mShowLeftCardWidth).toFloat()) mOnePageWidth = mCardWidth onScrolledChangedCallback() } } //计算mCurrentItemOffset private fun computeCurrentItemPos() { if (mOnePageWidth <= 0) return var pageChanged = false // 滑动超过一页说明已翻页 if (Math.abs(mCurrentItemOffset - currentItemPos * mOnePageWidth) >= mOnePageWidth) { pageChanged = true } if (pageChanged) { currentItemPos = mCurrentItemOffset / mOnePageWidth } } // RecyclerView位移事件监听, view大小随位移事件变化 private fun onScrolledChangedCallback() { if (mRecyclerView == null || mRecyclerView!!.adapter == null || mRecyclerView!!.layoutManager == null) return val offset = mCurrentItemOffset - currentItemPos * mOnePageWidth val percent = Math.max(Math.abs(offset) * 1.0 / mOnePageWidth, 0.0001).toFloat() val relate = currentItemPos + mBaseLine val currentView = mRecyclerView!!.layoutManager.findViewByPosition(relate) val rightView = mRecyclerView!!.layoutManager.findViewByPosition(relate + 1) val leftView = mRecyclerView!!.layoutManager.findViewByPosition(relate - 1) currentView?.scaleY = (mScale - 1) * percent + 1 leftView?.scaleY = (1 - mScale) * percent + mScale rightView?.scaleY = (1 - mScale) * percent + mScale } interface OnScalePositionChange { fun onCurrent(position: Int) } //滑动、松开手指 卡位 inner class CardLinearSnapHelper : LinearSnapHelper() { var lastPosition = 0 var mNoNeedToScroll = false override fun calculateDistanceToFinalSnap(layoutManager: RecyclerView.LayoutManager, targetView: View): IntArray? { val calculate = if (mNoNeedToScroll) intArrayOf(0, 0) else super.calculateDistanceToFinalSnap(layoutManager, targetView) if (calculate != null && !calculate.isEmpty()) { if (calculate[0] == 0) {//distanceToCenter == 0 不需要重新定位Y轴位置 mRecyclerView?.getChildLayoutPosition(targetView)?.apply { //找到停留的位置 if (this >= 0 && lastPosition != this) { lastPosition = this mPositionChangeListener?.onCurrent(this) } } } } return calculate } } } //计算设置每个card的高宽 class CardAdapterHelper { var mPagePadding = 15F var mShowLeftCardWidth = 15F var mScale = 0.9f // 两边视图scale fun onCreateViewHolder(parent: ViewGroup, itemView: View) { val lp = itemView.layoutParams as RecyclerView.LayoutParams lp.width = parent.width - DensityUtils.dip2px(2 * (mPagePadding + mShowLeftCardWidth)) itemView.layoutParams = lp } fun onBindViewHolder(itemView: View, position: Int, itemCount: Int) { val padding = DensityUtils.dip2px(mPagePadding) itemView.setPadding(padding, 0, padding, 0) val leftMarin = if (position == 0) padding + DensityUtils.dip2px(mShowLeftCardWidth) else 0 val rightMarin = if (position == itemCount - 1) padding + DensityUtils.dip2px(mShowLeftCardWidth) else 0 setViewMargin(itemView, leftMarin, 0, rightMarin, 0) itemView.scaleY = mScale } private fun setViewMargin(view: View, left: Int, top: Int, right: Int, bottom: Int) { val lp = view.layoutParams as ViewGroup.MarginLayoutParams if (lp.leftMargin != left || lp.topMargin != top || lp.rightMargin != right || lp.bottomMargin != bottom) { lp.setMargins(left, top, right, bottom) view.layoutParams = lp } } }其他公共函数
/** * - 提供网络分页请求的当前请求的序列号 * - 处理网络请求之后的页面更新(成功、失败)操作 */ abstract class DataSetter(val page: Int, val offset: Int) { var isLoading = false fun isFirstLoad() = (page == 1 && offset == 0) abstract fun success(data: List<Any>, offset: Int, hasMore: Boolean = false) abstract fun fail(message: String) } /** * 提供列表加载所需要的数据 */ interface DataSupporter { fun onLoad(setter: DataSetter) } /** * DataSupporter 的 LinearLayoutManager 实现实例 */ open class DataLinearLayoutManager(context: Context?) : LinearLayoutManager(context), DataSupporter { override fun onLoad(setter: DataSetter) { setter.success(ArrayList(), 0) } }