1 Star 0 Fork 0

Longdw/custom-layout-manager

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
CoverFlowLayoutManager.kt 11.57 KB
一键复制 编辑 原始数据 按行查看 历史
david.long@smart.com 提交于 1年前 . add final code
package com.smart.dhu.study_recyclerview
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.view.animation.LinearInterpolator
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.LayoutManager
import androidx.recyclerview.widget.RecyclerView.Recycler
import com.smart.dhu.study_recyclerview.StackLayoutManager.OnStackListener
import kotlin.math.abs
import kotlin.math.floor
/**
* @author david.long 2024/6/25 08:45
*/
class CoverFlowLayoutManager : LayoutManager() {
private val TAG = "CoverFlowLayoutManager"
private var pendingScrollPosition: Int = RecyclerView.NO_POSITION
private var childWidth = 0
/** 一次完整的聚焦滑动所需要移动的距离 */
private var onceCompleteScrollLength = -1f
/** 第一个item的偏移量 */
private var firstChildCompleteScrollLength = -1f
/** 屏幕可见的第一个item的position */
private var firstVisibleItemPosition = 0
/** 屏幕可见的最后一个item的position */
private var lastVisibleItemPosition = 0
/** item之间的间距 */
private var normalViewGap = 0
/** 水平方向累计偏移量 */
private var horizontalOffset = 0f
private var selectAnimator: ValueAnimator? = 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) {
if (pendingScrollPosition != RecyclerView.NO_POSITION) {
if (state.itemCount == 0) {
removeAndRecycleAllViews(recycler)
return
}
}
onceCompleteScrollLength = -1f
detachAndScrapAttachedViews(recycler)
if (onceCompleteScrollLength == -1f) {
//初始化一些常量
val itemView = recycler.getViewForPosition(0)
measureChildWithMargins(itemView, 0, 0)
childWidth = getDecoratedMeasurementHorizontal(itemView)
}
// 修正第一个可见view firstVisibleItemPosition 已经滑动了多少个完整的 onceCompleteScrollLength 就代表滑动了多少个item
firstChildCompleteScrollLength = width / 2f + childWidth / 2f
//重新布局
fill(recycler, state, 0)
}
private fun fill(recycler: RecyclerView.Recycler, state: RecyclerView.State, dx: Int): Int {
val resultDelta = fillHorizontal(recycler, state, dx)
return resultDelta
}
private fun fillHorizontal(recycler: RecyclerView.Recycler, state: RecyclerView.State, dx: Int): Int {
//边界检测
var tempDx = dx
if (dx < 0) {
//已到达左边界
if (horizontalOffset < 0) {
horizontalOffset = 0f
tempDx = 0
}
}
if (dx > 0) {
if (horizontalOffset >= getMaxOffset()) {
horizontalOffset = getMaxOffset()
tempDx = 0
}
}
//分离全部的view,加入到临时缓存
detachAndScrapAttachedViews(recycler)
var startX: Float
val fraction: Float
if (horizontalOffset >= firstChildCompleteScrollLength) {
//当第一个item不可见的时候
startX = normalViewGap.toFloat()
onceCompleteScrollLength = (childWidth + normalViewGap).toFloat()
firstVisibleItemPosition = floor(Math.abs(horizontalOffset - firstChildCompleteScrollLength) / onceCompleteScrollLength).toInt() + 1
fraction = (Math.abs(horizontalOffset - firstChildCompleteScrollLength) % onceCompleteScrollLength) / (onceCompleteScrollLength * 1.0f)
} else {
//当第一个item可见的时候
startX = getMinOffset()
firstVisibleItemPosition = 0
onceCompleteScrollLength = firstChildCompleteScrollLength
fraction = (Math.abs(horizontalOffset) % onceCompleteScrollLength) / (onceCompleteScrollLength * 1.0f)
}
// 临时将 lastVisibleItemPosition 赋值为getItemCount() - 1,放心,下面遍历时会判断view是否已溢出屏幕,并及时修正该值并结束布局
lastVisibleItemPosition = itemCount - 1
//每次布局之前都要对startX进行一次调整,初始的位置调整了,后面所有的view都是基于此位置进行布局的
val normalViewOffset = onceCompleteScrollLength * fraction
startX -= normalViewOffset
for (i in firstVisibleItemPosition until lastVisibleItemPosition) {
val item = recycler.getViewForPosition(i)
//1.添加view
addView(item)
//2.测量view
measureChildWithMargins(item, 0, 0)
val l = startX.toInt()
val t = paddingTop
val r = l + getDecoratedMeasurementHorizontal(item)
val b = t + getDecoratedMeasurementVertical(item)
//3.布局view
layoutChunk(item, l, t, r, b)
startX += childWidth + normalViewGap
//如果超出了边界就退出循环,不再添加view了
if (startX > width - paddingRight) {
lastVisibleItemPosition = i
break
}
}
return tempDx
}
private fun layoutChunk(item: View, l: Int, t: Int, r: Int, b: Int) {
// 缩放子view
// val minScale = 0.6f
val minScale = 1f
val currentScale: Float
//中间item中心点位置横坐标
val childCenterX = (r + l) / 2
val parentCenterX = width / 2
val isChildLayoutLeft = childCenterX <= parentCenterX
if (isChildLayoutLeft) {
//越往左边越小
val fractionScale = (parentCenterX - childCenterX) / (parentCenterX * 1.0f)
currentScale = 1.0f - (1.0f - minScale) * fractionScale
} else {
//越往右边越小
val fractionScale = (childCenterX - parentCenterX) / (parentCenterX * 1.0f)
currentScale = 1.0f - (1.0f - minScale) * fractionScale
}
item.scaleX = currentScale
item.scaleY = currentScale
item.setAlpha(currentScale)
layoutDecoratedWithMargins(item, l, t, r, b)
}
/**
* 获取某个childView在竖直方向所占的空间,将margin考虑进去
*/
private fun getDecoratedMeasurementVertical(view: View): Int {
val params = view.layoutParams as RecyclerView.LayoutParams
return (getDecoratedMeasuredHeight(view) + params.topMargin
+ params.bottomMargin)
}
/**
* 获取某个childView在水平方向所占的空间,将margin考虑进去
*/
private fun getDecoratedMeasurementHorizontal(view: View): Int {
val params = view.layoutParams as RecyclerView.LayoutParams
return getDecoratedMeasuredWidth(view) + params.leftMargin + params.rightMargin
}
/**
* 最小偏移量,中间的item距离最左边的距离
*/
private fun getMinOffset(): Float {
if (childWidth == 0) {
return 0f
}
return (width - childWidth) / 2f
}
/**
* 最大偏移量
*/
private fun getMaxOffset(): Float {
if (childWidth == 0 || itemCount == 0) return 0f
return ((childWidth + normalViewGap) * (itemCount - 1)).toFloat()
}
override fun scrollToPosition(position: Int) {
pendingScrollPosition = position
}
override fun onLayoutCompleted(state: RecyclerView.State) {
pendingScrollPosition = RecyclerView.NO_POSITION
}
override fun canScrollHorizontally(): Boolean {
return true
}
override fun scrollHorizontallyBy(
dx: Int,
recycler: RecyclerView.Recycler,
state: RecyclerView.State
): Int {
// 手指从右向左滑动,dx > 0; 手指从左向右滑动,dx < 0;
// 位移0、没有子View 当然不移动
if (dx == 0 || childCount == 0) {
return 0
}
val realDx = dx / 1.0f
if (abs(realDx) < 0.00000001f) {
return 0
}
horizontalOffset += dx
val tempDx = fill(recycler, state, dx)
return tempDx
}
override fun onScrollStateChanged(state: Int) {
super.onScrollStateChanged(state)
when (state) {
//当手指按下时,停止当前正在播放的动画
RecyclerView.SCROLL_STATE_DRAGGING ->
cancelAnimator()
RecyclerView.SCROLL_STATE_IDLE -> {
//当列表滚动停止后
//找到离目标落点最近的item索引
smoothScrollToPosition(findShouldSelectPosition(), null)
}
}
}
private fun findShouldSelectPosition(): Int {
if (onceCompleteScrollLength == -1f || firstVisibleItemPosition == -1) {
return -1
}
val position = (Math.abs(horizontalOffset) / (childWidth + normalViewGap))
// 超过一半,应当选中下一项
if ((Math.abs(horizontalOffset) % (childWidth + normalViewGap)) >= (childWidth + normalViewGap) / 2.0f) {
if (position + 1 <= itemCount - 1) {
return (position + 1).toInt()
}
}
return position.toInt()
}
/**
* 平滑滚动到某个位置
*
* @param position 目标Item索引
*/
fun smoothScrollToPosition(position: Int, listener: OnStackListener?) {
if (position > -1 && position < itemCount) {
startValueAnimator(position, listener)
}
}
/**
* 取消动画
*/
fun cancelAnimator() {
if (selectAnimator != null && (selectAnimator?.isStarted == true || selectAnimator?.isRunning == true)) {
selectAnimator?.cancel()
}
}
private fun startValueAnimator(position: Int, listener: OnStackListener?) {
cancelAnimator()
val distance = getScrollToPositionOffset(position)
val minDuration = 100
val maxDuration = 300
val duration: Long
val distanceFraction = ((abs(distance.toDouble()) / (childWidth + normalViewGap)).toFloat())
Log.i(TAG, "horizontalOffset-->$horizontalOffset, distance-->$distance, distanceFraction-->$distanceFraction")
duration = if (distance <= (childWidth + normalViewGap)) {
(minDuration + (maxDuration - minDuration) * distanceFraction).toLong()
} else {
(maxDuration * distanceFraction).toLong()
}
selectAnimator = ValueAnimator.ofFloat(0.0f, distance)
selectAnimator?.setDuration(duration)
selectAnimator?.interpolator = LinearInterpolator()
val startedOffset = horizontalOffset
selectAnimator?.addUpdateListener { animation ->
val value = animation.animatedValue as Float
horizontalOffset = startedOffset + value
requestLayout()
}
selectAnimator?.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
super.onAnimationEnd(animation)
listener?.onFocusAnimEnd()
}
})
selectAnimator?.start()
}
/**
* 滑动到指定的位置需要移动的距离
* @param position
* @return
*/
private fun getScrollToPositionOffset(position: Int): Float {
return position * (childWidth + normalViewGap) - Math.abs(horizontalOffset)
}
}
Loading...
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/longdw/custom-layout-manager.git
git@gitee.com:longdw/custom-layout-manager.git
longdw
custom-layout-manager
custom-layout-manager
master

搜索帮助