【光环助手V5.5.0】首页顶部tab-游戏单广场(UI)https://git.ghzs.com/pm/halo-app-issues/-/issues/1599
【光环助手V5.5.0】首页/版块内容列表-游戏单广场(UI)https://git.ghzs.com/pm/halo-app-issues/-/issues/1600
This commit is contained in:
leafwai
2021-11-15 17:27:41 +08:00
parent 32bc1a4a6f
commit 4e78162d7f
30 changed files with 2551 additions and 14 deletions

View File

@ -0,0 +1,23 @@
package com.gh.common.view
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import androidx.recyclerview.widget.RecyclerView
class StackRecyclerView : RecyclerView {
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
)
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
parent.requestDisallowInterceptTouchEvent(true)
return super.dispatchTouchEvent(ev)
}
}

View File

@ -0,0 +1,22 @@
package com.gh.common.view.stacklayoutmanager
import android.view.View
import com.gh.common.view.stacklayoutmanager.StackLayoutManager.ScrollOrientation
abstract class StackAnimation(scrollOrientation: ScrollOrientation, visibleCount: Int) {
protected val mScrollOrientation = scrollOrientation
protected var mVisibleCount = visibleCount
internal fun setVisibleCount(visibleCount: Int) {
mVisibleCount = visibleCount
}
/**
* 外部回调,用来做动画.
* @param firstMovePercent 第一个可视 item 移动的百分比,当即将完全移出屏幕的时候 firstMovePercent无限接近1.
* @param itemView 当前的 itemView.
* @param position 当前 itemView 对应的位置position = 0 until visibleCount.
*/
abstract fun doAnimation(firstMovePercent: Float, itemView: View, position: Int)
}

View File

@ -0,0 +1,34 @@
package com.gh.common.view.stacklayoutmanager
import android.view.View
abstract class StackLayout(scrollOrientation: StackLayoutManager.ScrollOrientation,
visibleCount: Int,
perItemOffset: Int) {
protected val mScrollOrientation = scrollOrientation
protected var mVisibleCount = visibleCount
protected var mPerItemOffset = perItemOffset
internal fun setItemOffset(offset: Int) {
mPerItemOffset = offset
}
internal fun getItemOffset(): Int {
return mPerItemOffset
}
/**
* 外部回调,用来做布局.
* @param firstMovePercent 第一个可视 item 移动的百分比,当即将完全移出屏幕的时候 firstMovePercent无限接近1.
* @param itemView 当前的 itemView.
* @param position 当前 itemView 对应的位置position = 0 until visibleCount.
*/
abstract fun doLayout(stackLayoutManager: StackLayoutManager,
scrollOffset: Int,
firstMovePercent: Float,
itemView: View,
position: Int)
abstract fun requestLayout()
}

View File

@ -0,0 +1,544 @@
package com.gh.common.view.stacklayoutmanager
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.*
import com.gh.common.util.DisplayUtils
import com.gh.common.util.dip2px
import com.lightgame.utils.Utils
import kotlin.math.ceil
import kotlin.math.floor
class StackLayoutManager(scrollOrientation: ScrollOrientation,
visibleCount: Int,
animation: Class<out StackAnimation>,
layout: Class<out StackLayout>) : RecyclerView.LayoutManager() {
private enum class FlingOrientation{NONE, LEFT_TO_RIGHT, RIGHT_TO_LEFT, TOP_TO_BOTTOM, BOTTOM_TO_TOP}
enum class ScrollOrientation{LEFT_TO_RIGHT, RIGHT_TO_LEFT, TOP_TO_BOTTOM, BOTTOM_TO_TOP}
private var mVisibleItemCount = visibleCount
private var mScrollOrientation = scrollOrientation
private var mScrollOffset: Int
var isFlinging = false
private lateinit var mOnScrollListener: OnScrollListener
private lateinit var mOnFlingListener: OnFlingListener
//做动画的组件,支持自定义
private var mAnimation: StackAnimation? = null
//做布局的组件,支持自定义
private var mLayout: StackLayout? = null
//是否是翻页效果
private var mPagerMode = true
//触发翻页效果的最低 Fling速度
private var mPagerFlingVelocity = 0
//标志当前滚动是否是调用scrollToCenter之后触发的滚动
private var mFixScrolling = false
//fling的方向用来判断是前翻还是后翻
private var mFlingOrientation = FlingOrientation.NONE
//当前所处item对应的位置
private var itemPosition = 0
//判断item位置是否发生了改变
private var isItemPositionChanged = false
//item 位置发生改变的回调
private var itemChangedListener: ItemChangedListener? = null
interface ItemChangedListener {
fun onItemChanged(position: Int)
}
/**
* 设置是否为ViewPager 式翻页模式.
* <p>
* 当设置为 true 的时候,可以配合[StackLayoutManager.setPagerFlingVelocity]设置触发翻页的最小速度.
* @param isPagerMode 这个值默认是 false当设置为 true 的时候,会有 viewPager 翻页效果.
*/
fun setPagerMode(isPagerMode: Boolean) {
mPagerMode = isPagerMode
}
/**
* @return 当前是否为ViewPager翻页模式.
*/
fun getPagerMode(): Boolean {
return mPagerMode
}
/**
* 设置触发ViewPager翻页效果的最小速度.
* <p>
* 该值仅在 [StackLayoutManager.getPagerMode] == true的时候有效.
* @param velocity 默认值是2000.
*/
fun setPagerFlingVelocity(@androidx.annotation.IntRange(from = 0, to = Int.MAX_VALUE.toLong()) velocity: Int) {
mPagerFlingVelocity = Int.MAX_VALUE.coerceAtMost(0.coerceAtLeast(velocity))
}
/**
* @return 当前触发翻页的最小 fling 速度.
*/
fun getPagerFlingVelocity(): Int {
return mPagerFlingVelocity
}
/**
* 设置recyclerView 静止时候可见的itemView 个数.
* @param count 可见 itemView默认为3
*/
fun setVisibleItemCount(@androidx.annotation.IntRange(from = 1, to = Long.MAX_VALUE)count: Int) {
mVisibleItemCount = (itemCount - 1).coerceAtMost(1.coerceAtLeast(count))
mAnimation?.setVisibleCount(mVisibleItemCount)
}
/**
* 获取recyclerView 静止时候可见的itemView 个数.
* @return 静止时候可见的itemView 个数默认为3.
*/
fun getVisibleItemCount(): Int {
return mVisibleItemCount
}
/**
* 设置 item 偏移值,即第 i 个 item 相对于 第 i-1个 item 在水平方向的偏移值默认是40px.
* @param offset 每个 item 相对于前一个的偏移值.
*/
fun setItemOffset(offset: Int) {
mLayout?.setItemOffset(offset)
}
/**
* 获取每个 item 相对于前一个的水平偏移值.
* @return 每个 item 相对于前一个的水平偏移值.
*/
fun getItemOffset(): Int {
return if (mLayout == null) {
0
} else {
mLayout!!.getItemOffset()
}
}
/**
* 设置item 移动动画.
* @param animation item 移动动画.
*/
fun setAnimation(animation: StackAnimation) {
mAnimation = animation
}
/**
* 获取 item 移动动画.
* @return item 移动动画.
*/
fun getAnimation(): StackAnimation? {
return mAnimation
}
/**
* 获取StackLayoutManager 的滚动方向.
* @return StackLayoutManager 的滚动方向.
*/
fun getScrollOrientation(): ScrollOrientation {
return mScrollOrientation
}
/**
* 返回第一个可见 itemView 的位置.
* @return 返回第一个可见 itemView 的位置.
*/
fun getFirstVisibleItemPosition(): Int {
if (width == 0 || height == 0) {
return 0
}
return when(mScrollOrientation) {
ScrollOrientation.RIGHT_TO_LEFT -> floor((mScrollOffset * 1.0 / width)).toInt()
ScrollOrientation.LEFT_TO_RIGHT -> itemCount - 1 - ceil((mScrollOffset * 1.0 / width)).toInt()
ScrollOrientation.BOTTOM_TO_TOP -> floor((mScrollOffset * 1.0 / height)).toInt()
ScrollOrientation.TOP_TO_BOTTOM -> itemCount - 1 - ceil((mScrollOffset * 1.0 / height)).toInt()
}
}
/**
* 设置 item 位置改变时触发的回调
*/
fun setItemChangedListener(listener: ItemChangedListener) {
itemChangedListener = listener
}
// constructor(scrollOrientation: ScrollOrientation) : this(scrollOrientation, 3, DefaultAnimation::class.java, DefaultLayout::class.java)
//
// constructor(scrollOrientation: ScrollOrientation, visibleCount: Int) : this(scrollOrientation, visibleCount, DefaultAnimation::class.java, DefaultLayout::class.java)
// constructor() : this(ScrollOrientation.RIGHT_TO_LEFT)
init {
mScrollOffset = when(mScrollOrientation) {
ScrollOrientation.RIGHT_TO_LEFT, ScrollOrientation.BOTTOM_TO_TOP -> 0
else -> Int.MAX_VALUE
}
if (StackAnimation::class.java.isAssignableFrom(animation)) {
try {
val cla = animation.getDeclaredConstructor(ScrollOrientation::class.java, Int::class.javaPrimitiveType)
mAnimation = cla.newInstance(scrollOrientation, visibleCount) as StackAnimation
} catch (e: Exception) {
e.printStackTrace()
}
}
if (StackLayout::class.java.isAssignableFrom(layout)) {
try {
val cla = layout.getDeclaredConstructor(ScrollOrientation::class.java, Int::class.javaPrimitiveType, Int::class.javaPrimitiveType)
mLayout = cla.newInstance(scrollOrientation, visibleCount, 30) as StackLayout
} catch (e: Exception) {
e.printStackTrace()
}
}
}
override fun generateDefaultLayoutParams(): LayoutParams {
return LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT)
}
override fun onAttachedToWindow(view: RecyclerView) {
super.onAttachedToWindow(view)
mOnFlingListener = object : OnFlingListener() {
override fun onFling(velocityX: Int, velocityY: Int): Boolean {
if (mPagerMode) {
when(mScrollOrientation) {
ScrollOrientation.RIGHT_TO_LEFT, ScrollOrientation.LEFT_TO_RIGHT -> {
mFlingOrientation = when {
velocityX > mPagerFlingVelocity -> FlingOrientation.RIGHT_TO_LEFT
velocityX < -mPagerFlingVelocity -> FlingOrientation.LEFT_TO_RIGHT
else -> FlingOrientation.NONE
}
if (mScrollOffset in 1 until width * (itemCount - 1)) { //边界不需要滚动
mFixScrolling = true
}
}
else -> {
mFlingOrientation = when {
velocityY > mPagerFlingVelocity -> FlingOrientation.BOTTOM_TO_TOP
velocityY < -mPagerFlingVelocity -> FlingOrientation.TOP_TO_BOTTOM
else -> FlingOrientation.NONE
}
if (mScrollOffset in 1 until width * (itemCount - 1)) { //边界不需要滚动
mFixScrolling = true
}
}
}
calculateAndScrollToTarget(view)
}
else {
Utils.log("$velocityX $velocityY")
}
return mPagerMode
}
}
view.onFlingListener = mOnFlingListener
mOnScrollListener = object : OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (newState == SCROLL_STATE_IDLE) {
isFlinging = false
if (!mFixScrolling) {
mFixScrolling = true
calculateAndScrollToTarget(view)
} else {
//表示此次 IDLE 是由修正位置结束触发的
mFixScrolling = false
}
} else if (newState == SCROLL_STATE_DRAGGING) {
mFixScrolling = false
isFlinging = false
} else if (newState == SCROLL_STATE_SETTLING) {
isFlinging = true
}
}
}
view.addOnScrollListener(mOnScrollListener)
}
override fun onDetachedFromWindow(view: RecyclerView?, recycler: RecyclerView.Recycler?) {
super.onDetachedFromWindow(view, recycler)
if (view?.onFlingListener == mOnFlingListener) {
view.onFlingListener = null
}
view?.removeOnScrollListener(mOnScrollListener)
}
override fun canScrollHorizontally(): Boolean {
if (itemCount == 0) {
return false
}
return when (mScrollOrientation) {
ScrollOrientation.LEFT_TO_RIGHT, ScrollOrientation.RIGHT_TO_LEFT -> true
else -> false
}
}
override fun canScrollVertically(): Boolean {
if (itemCount == 0) {
return false
}
return when (mScrollOrientation) {
ScrollOrientation.TOP_TO_BOTTOM, ScrollOrientation.BOTTOM_TO_TOP -> true
else -> false
}
}
override fun onLayoutChildren(recycler: RecyclerView.Recycler, state: State) {
mLayout?.requestLayout()
removeAndRecycleAllViews(recycler)
if (itemCount > 0) {
mScrollOffset = getValidOffset(mScrollOffset)
loadItemView(recycler)
}
}
override fun scrollHorizontallyBy(dx: Int, recycler: RecyclerView.Recycler, state: State): Int {
return handleScrollBy(dx, recycler)
}
override fun scrollVerticallyBy(dy: Int, recycler: RecyclerView.Recycler, state: State?): Int {
return handleScrollBy(dy, recycler)
}
override fun scrollToPosition(position: Int) {
if (position < 0 || position >= itemCount) {
throw ArrayIndexOutOfBoundsException("$position is out of bound [0..$itemCount-1]")
}
if (position < 0) {
throw ArrayIndexOutOfBoundsException("$position is out of bound [0..$itemCount-1]")
}
mScrollOffset = getPositionOffset(position)
requestLayout()
}
override fun smoothScrollToPosition(recyclerView: RecyclerView, state: State?, position: Int) {
if (position < 0 || position >= itemCount) {
throw ArrayIndexOutOfBoundsException("$position is out of bound [0..$itemCount-1]")
}
if (position < 0) {
throw ArrayIndexOutOfBoundsException("$position is out of bound [0..$itemCount-1]")
}
mFixScrolling = true
scrollToCenter(position, recyclerView, true)
}
private fun updatePositionRecordAndNotify(position: Int) {
if (itemChangedListener == null) {
return
}
if (position != itemPosition) {
isItemPositionChanged = true
itemPosition = position
itemChangedListener?.onItemChanged(itemPosition)
} else {
isItemPositionChanged = false
}
}
private fun handleScrollBy(offset: Int, recycler: RecyclerView.Recycler): Int {
//期望值,不得超过最大最小值,所以期望值不一定等于实际值
val expectOffset = mScrollOffset + offset
//实际值
mScrollOffset = getValidOffset(expectOffset)
//实际偏移超过最大最小值之后的偏移都应该是0该值作为返回值否则在极限位置进行滚动的时候不会出现弹性阴影
val exactMove = mScrollOffset - expectOffset + offset
if (exactMove == 0) {
//itemViews 位置都不会改变,直接 return
return 0
}
detachAndScrapAttachedViews(recycler)
loadItemView(recycler)
return offset
}
private fun loadItemView(recycler: RecyclerView.Recycler) {
val firstVisiblePosition = getFirstVisibleItemPosition()
val lastVisiblePosition = getLastVisibleItemPosition()
//位移百分比
val movePercent = getFirstVisibleItemMovePercent()
for (i in lastVisiblePosition downTo firstVisiblePosition) {
val view = recycler.getViewForPosition(i)
view.layoutParams = view.layoutParams.apply { width = DisplayUtils.getScreenWidth() - 50F.dip2px() }
//添加到recycleView 中
addView(view)
//测量
measureChild(view, 0, 0)
//布局
mLayout?.doLayout(this, mScrollOffset, movePercent, view, i - firstVisiblePosition)
//做动画
mAnimation?.doAnimation(movePercent, view, i - firstVisiblePosition)
}
//尝试更新当前item的位置并通知外界
updatePositionRecordAndNotify(firstVisiblePosition)
//重用
if (firstVisiblePosition - 1 >= 0) {
val view = recycler.getViewForPosition(firstVisiblePosition - 1)
resetViewAnimateProperty(view)
removeAndRecycleView(view, recycler)
}
if (lastVisiblePosition + 1 < itemCount) {
val view = recycler.getViewForPosition(lastVisiblePosition + 1)
resetViewAnimateProperty(view)
removeAndRecycleView(view, recycler)
}
}
private fun resetViewAnimateProperty(view: View) {
view.rotationY = 0f
view.rotationX = 0f
view.scaleX = 1f
view.scaleY = 1f
view.alpha = 1f
}
private fun calculateAndScrollToTarget(view: RecyclerView) {
val targetPosition = calculateCenterPosition(getFirstVisibleItemPosition())
scrollToCenter(targetPosition, view, true)
}
private fun scrollToCenter(targetPosition: Int, recyclerView: RecyclerView, animation: Boolean) {
val targetOffset = getPositionOffset(targetPosition)
when(mScrollOrientation) {
ScrollOrientation.LEFT_TO_RIGHT, ScrollOrientation.RIGHT_TO_LEFT -> {
if (animation) {
recyclerView.smoothScrollBy(targetOffset - mScrollOffset, 0)
} else {
recyclerView.scrollBy(targetOffset - mScrollOffset, 0)
}
}
else -> {
if (animation) {
recyclerView.smoothScrollBy(0, targetOffset - mScrollOffset)
} else {
recyclerView.scrollBy(0, targetOffset - mScrollOffset)
}
}
}
}
private fun getValidOffset(expectOffset: Int): Int {
val offset = (width * (itemCount - 1)).coerceAtMost(expectOffset).coerceAtLeast(0)
return when(mScrollOrientation) {
ScrollOrientation.RIGHT_TO_LEFT, ScrollOrientation.LEFT_TO_RIGHT -> offset
else -> (height * (itemCount - 1)).coerceAtMost(expectOffset).coerceAtLeast(0)
}
}
private fun getPositionOffset(position: Int): Int {
return when(mScrollOrientation) {
ScrollOrientation.RIGHT_TO_LEFT -> position * width
ScrollOrientation.LEFT_TO_RIGHT -> (itemCount - 1 - position) * width
ScrollOrientation.BOTTOM_TO_TOP -> position * height
ScrollOrientation.TOP_TO_BOTTOM -> (itemCount - 1 - position) * height
}
}
private fun getLastVisibleItemPosition(): Int {
val firstVisiblePosition = getFirstVisibleItemPosition()
return if (firstVisiblePosition + mVisibleItemCount > itemCount - 1) {
itemCount - 1
} else {
firstVisiblePosition + mVisibleItemCount
}
}
private fun getFirstVisibleItemMovePercent(): Float {
if (width == 0 || height == 0) {
return 0f
}
return when (mScrollOrientation) {
ScrollOrientation.RIGHT_TO_LEFT -> (mScrollOffset % width) * 1.0f / width
ScrollOrientation.LEFT_TO_RIGHT -> {
val targetPercent = 1 - (mScrollOffset % width) * 1.0f / width
return if (targetPercent == 1f) {
0f
} else {
targetPercent
}
}
ScrollOrientation.BOTTOM_TO_TOP -> (mScrollOffset % height) * 1.0f / height
ScrollOrientation.TOP_TO_BOTTOM -> {
val targetPercent = 1 - (mScrollOffset % height) * 1.0f / height
return if (targetPercent == 1f) {
0f
} else {
targetPercent
}
}
}
}
private fun calculateCenterPosition(position: Int): Int {
//当是 Fling 触发的时候
val triggerOrientation = mFlingOrientation
mFlingOrientation = FlingOrientation.NONE
when(mScrollOrientation) {
ScrollOrientation.RIGHT_TO_LEFT -> {
if (triggerOrientation == FlingOrientation.RIGHT_TO_LEFT) {
return position + 1
} else if (triggerOrientation == FlingOrientation.LEFT_TO_RIGHT) {
return position
}
}
ScrollOrientation.LEFT_TO_RIGHT -> {
if (triggerOrientation == FlingOrientation.LEFT_TO_RIGHT) {
return position + 1
} else if (triggerOrientation == FlingOrientation.RIGHT_TO_LEFT) {
return position
}
}
ScrollOrientation.BOTTOM_TO_TOP -> {
if (triggerOrientation == FlingOrientation.BOTTOM_TO_TOP) {
return position + 1
} else if (triggerOrientation == FlingOrientation.TOP_TO_BOTTOM) {
return position
}
}
ScrollOrientation.TOP_TO_BOTTOM -> {
if (triggerOrientation == FlingOrientation.TOP_TO_BOTTOM) {
return position + 1
} else if (triggerOrientation == FlingOrientation.BOTTOM_TO_TOP) {
return position
}
}
}
//当不是 fling 触发的时候
val percent = getFirstVisibleItemMovePercent()
//向左移动超过50% position(firstVisibleItemPosition)++
//否 position不变
return if (percent < 0.5) {
position
} else {
position + 1
}
}
}

View File

@ -0,0 +1,51 @@
package com.gh.gamecenter.gamecollection.square
import android.content.Context
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.gh.gamecenter.databinding.GameCollectionAmwayContentItemBinding
import com.gh.gamecenter.entity.AmwayCommentEntity
import com.lightgame.adapter.BaseRecyclerAdapter
class GameCollectionAmwayAdapter(context: Context) :
BaseRecyclerAdapter<RecyclerView.ViewHolder>(context) {
private var mAmwayList = ArrayList<AmwayCommentEntity>()
fun setAmwayList(amwayList: ArrayList<AmwayCommentEntity>) {
mAmwayList = amwayList
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
GameCollectionAmwayContentItemViewHolder(GameCollectionAmwayContentItemBinding.inflate(mLayoutInflater, parent, false))
override fun getItemCount(): Int = if (getRealCount() > 1) getRealCount() + INCREASE_COUNT else getRealCount()
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is GameCollectionAmwayContentItemViewHolder) {
holder.binding.entity = mAmwayList[getRealPosition(position)]
}
}
class GameCollectionAmwayContentItemViewHolder(var binding: GameCollectionAmwayContentItemBinding) :
RecyclerView.ViewHolder(binding.root)
fun getRealCount(): Int = mAmwayList.size
fun getRealPosition(position: Int) = when (position) {
0 -> {
getRealCount() - 1
}
getRealCount() + 1 -> {
0
}
else -> {
position - 1
}
}
companion object{
const val INCREASE_COUNT = 2
}
}

View File

@ -0,0 +1,111 @@
package com.gh.gamecenter.gamecollection.square
import android.animation.Animator
import android.animation.TimeInterpolator
import android.animation.ValueAnimator
import android.view.animation.AccelerateDecelerateInterpolator
import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2
import com.gh.common.util.DirectUtils
import com.gh.gamecenter.databinding.GameCollectionSquareAmwayItemBinding
import java.lang.ref.WeakReference
class GameCollectionAmwayViewHolder(var binding: GameCollectionSquareAmwayItemBinding) : RecyclerView.ViewHolder(binding.root) {
val mAdapter = GameCollectionAmwayAdapter(binding.root.context).apply { registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
override fun onChanged() {
if (itemCount <= 1) {
stop()
} else {
start()
}
}
}) }
val mLoopTask = object : Runnable {
private val reference: WeakReference<ViewPager2> = WeakReference(binding.amwayVp)
override fun run() {
val vp: ViewPager2? = reference.get()
if (vp != null) {
val count = mAdapter.itemCount
if (count == 0) return
val next = (vp.currentItem + 1) % count
vp.setCurrentItem(next, 1000)
vp.postDelayed(this, GameCollectionSquareAdapter.AMWAY_LOOP_TIME)
}
}
}
fun bindAmway() {
binding.amwayVp.run {
if (adapter is GameCollectionAmwayAdapter) return
isUserInputEnabled = false
adapter = mAdapter
orientation = ViewPager2.ORIENTATION_VERTICAL
registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
private var mTempPosition = GameCollectionSquareAdapter.INVALID_VALUE
private var isScrolled = false
override fun onPageSelected(position: Int) {
if (isScrolled) {
mTempPosition = position
}
}
override fun onPageScrollStateChanged(state: Int) {
//手势滑动中,代码执行滑动中
if (state == ViewPager2.SCROLL_STATE_DRAGGING || state == ViewPager2.SCROLL_STATE_SETTLING) {
isScrolled = true
} else if (state == ViewPager2.SCROLL_STATE_IDLE) {
//滑动闲置或滑动结束
isScrolled = false
if (mTempPosition != GameCollectionSquareAdapter.INVALID_VALUE) {
if (mTempPosition == 0) {
setCurrentItem(mAdapter.getRealCount(), false)
} else if (mTempPosition == mAdapter.itemCount - 1) {
setCurrentItem(1, false)
}
}
}
}
})
setCurrentItem(1, false)
postDelayed(mLoopTask, GameCollectionSquareAdapter.AMWAY_LOOP_TIME)
}
binding.root.setOnClickListener { DirectUtils.directToAmway(binding.root.context) }
}
fun start() {
stop()
binding.amwayVp.postDelayed(mLoopTask, GameCollectionSquareAdapter.AMWAY_LOOP_TIME)
}
fun stop() {
binding.amwayVp.removeCallbacks(mLoopTask)
}
fun ViewPager2.setCurrentItem(
item: Int,
duration: Long,
interpolator: TimeInterpolator = AccelerateDecelerateInterpolator(),
pagePxHeight: Int = height
) {
val pxToDrag: Int = pagePxHeight * (item - currentItem)
val animator = ValueAnimator.ofInt(0, pxToDrag)
var previousValue = 0
animator.addUpdateListener { valueAnimator ->
val currentValue = valueAnimator.animatedValue as Int
val currentPxToDrag = (currentValue - previousValue).toFloat()
fakeDragBy(-currentPxToDrag)
previousValue = currentValue
}
animator.addListener(object : Animator.AnimatorListener {
override fun onAnimationStart(animation: Animator?) { beginFakeDrag() }
override fun onAnimationEnd(animation: Animator?) { endFakeDrag() }
override fun onAnimationCancel(animation: Animator?) { }
override fun onAnimationRepeat(animation: Animator?) { }
})
animator.interpolator = interpolator
animator.duration = duration
animator.start()
}
}

View File

@ -0,0 +1,23 @@
package com.gh.gamecenter.gamecollection.square
import android.os.Bundle
import com.gh.base.BaseActivity
import com.gh.common.util.DisplayUtils
import com.gh.gamecenter.R
class GameCollectionSquareActivity : BaseActivity() {
override fun getLayoutId(): Int {
return R.layout.activity_amway
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
DisplayUtils.transparentStatusBar(this)
val containerFragment = supportFragmentManager.findFragmentByTag(
GameCollectionSquareFragment::class.java.simpleName)
?: GameCollectionSquareFragment().with(intent.extras)
supportFragmentManager.beginTransaction().replace(R.id.placeholder, containerFragment, GameCollectionSquareFragment::class.java.simpleName).commitAllowingStateLoss()
}
}

View File

@ -0,0 +1,128 @@
package com.gh.gamecenter.gamecollection.square
import android.content.Context
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.gh.common.constant.ItemViewType
import com.gh.common.exposure.ExposureEvent
import com.gh.common.exposure.ExposureSource
import com.gh.common.exposure.IExposable
import com.gh.common.util.DisplayUtils
import com.gh.common.util.toDrawable
import com.gh.gamecenter.R
import com.gh.gamecenter.adapter.viewholder.FooterViewHolder
import com.gh.gamecenter.baselist.ListAdapter
import com.gh.gamecenter.databinding.GameCollectionSquareAmwayItemBinding
import com.gh.gamecenter.databinding.GameCollectionSquareItemBinding
import com.gh.gamecenter.entity.GamesCollectionEntity
class GameCollectionSquareAdapter(
context: Context,
private val isHome: Boolean = false,
private val mViewModel: GameCollectionSquareViewModel,
private var basicExposureSource: List<ExposureSource>
) :
ListAdapter<GamesCollectionEntity>(context), IExposable {
override fun getItemViewType(position: Int): Int {
return if (isHome && position == 0) ItemViewType.ITEM_HEADER else if (position == itemCount - 1) ItemViewType.ITEM_FOOTER else ItemViewType.ITEM_BODY
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
ItemViewType.ITEM_HEADER -> {
GameCollectionAmwayViewHolder(
GameCollectionSquareAmwayItemBinding.bind(
mLayoutInflater.inflate(
R.layout.game_collection_square_amway_item,
parent,
false
)
)
)
}
ItemViewType.ITEM_BODY -> {
GameCollectionSquareItemViewHolder(
GameCollectionSquareItemBinding.bind(
mLayoutInflater.inflate(
R.layout.game_collection_square_item,
parent,
false
)
)
)
}
ItemViewType.ITEM_FOOTER -> FooterViewHolder(
mLayoutInflater.inflate(
R.layout.refresh_footerview,
parent,
false
)
)
else -> GameCollectionSquareItemViewHolder(
GameCollectionSquareItemBinding.bind(
mLayoutInflater.inflate(
R.layout.game_collection_square_item,
parent,
false
)
)
)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is GameCollectionAmwayViewHolder -> {
holder.bindAmway()
}
is GameCollectionSquareItemViewHolder -> {
val realPosition = if (isHome) position - 1 else position
holder.bindGameCollection(mEntityList[realPosition])
}
is FooterViewHolder -> {
holder.initItemPadding()
holder.initFooterViewHolder(mViewModel, mIsLoading, mIsNetworkError, mIsOver)
holder.hint.setTextColor(ContextCompat.getColor(mContext, R.color.text_B3B3B3))
val lp = holder.itemView.layoutParams as RecyclerView.LayoutParams
lp.height = DisplayUtils.dip2px(48F)
lp.width = ViewGroup.LayoutParams.MATCH_PARENT
holder.itemView.layoutParams = lp
}
}
}
override fun getItemCount() = if (mEntityList.isNullOrEmpty()) 0 else if (isHome) mEntityList.size + 2 else mEntityList.size + 1
class GameCollectionSquareItemViewHolder(var binding: GameCollectionSquareItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bindGameCollection(gamesCollectionEntity: GamesCollectionEntity) {
binding.run {
entity = gamesCollectionEntity
stampIv.setImageDrawable(if (gamesCollectionEntity.stamp == "offical") R.drawable.ic_official.toDrawable() else R.drawable.ic_chosen.toDrawable())
}
}
}
companion object {
const val INVALID_VALUE = -1
// 安利墙卡片轮播时间
const val AMWAY_LOOP_TIME = 5000L
}
override fun getEventByPosition(pos: Int): ExposureEvent? {
// return mEntityList[pos].exposureEvent
return null
}
override fun getEventListByPosition(pos: Int): List<ExposureEvent>? {
// return mEntityList[pos].exposureEventList
return null
}
}

View File

@ -0,0 +1,313 @@
package com.gh.gamecenter.gamecollection.square
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.RecyclerView
import com.gh.common.TimeElapsedHelper
import com.gh.common.exposure.ExposureListener
import com.gh.common.exposure.ExposureSource
import com.gh.common.util.*
import com.gh.common.view.VerticalItemDecoration
import com.gh.download.DownloadManager
import com.gh.gamecenter.R
import com.gh.gamecenter.baselist.LazyListFragment
import com.gh.gamecenter.baselist.ListAdapter
import com.gh.gamecenter.databinding.FragmentGameCollectionSquareAlBinding
import com.gh.gamecenter.databinding.FragmentGameCollectionSquareBinding
import com.gh.gamecenter.entity.GamesCollectionEntity
import com.gh.gamecenter.eventbus.EBDownloadStatus
import com.gh.gamecenter.eventbus.EBPackage
import com.gh.gamecenter.eventbus.EBReuse
import com.gh.gamecenter.gamecollection.publish.GameCollectionEditActivity
import com.gh.gamecenter.gamecollection.tag.GameCollectionTagSelectActivity
import com.gh.gamecenter.personal.PersonalFragment
import com.google.android.material.appbar.AppBarLayout
import com.lightgame.download.DataWatcher
import com.lightgame.download.DownloadEntity
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import kotlin.math.abs
class GameCollectionSquareFragment : LazyListFragment<GamesCollectionEntity, GameCollectionSquareViewModel>() {
private lateinit var mViewModel: GameCollectionSquareViewModel
private val mElapsedHelper by lazy { TimeElapsedHelper() }
private lateinit var mExposureListener: ExposureListener
private lateinit var mDefaultBinding: FragmentGameCollectionSquareBinding
private lateinit var mAlternativeBinding: FragmentGameCollectionSquareAlBinding
private var mAdapter: GameCollectionSquareAdapter? = null
private var mUseAlternativeLayout = false
private val dataWatcher = object : DataWatcher() {
override fun onDataChanged(downloadEntity: DownloadEntity) {
// mAdapter?.notifyItemByDownload(downloadEntity)
// if (downloadEntity.meta[XapkInstaller.XAPK_UNZIP_STATUS] == XapkUnzipStatus.FAILURE.name) {
// showUnzipFailureDialog(downloadEntity)
// }
}
}
override fun onCreate(savedInstanceState: Bundle?) {
mUseAlternativeLayout = arguments?.getBoolean(EntranceUtils.KEY_IS_HOME, false) ?: false
super.onCreate(savedInstanceState)
}
override fun getLayoutId() = R.layout.fragment_stub
override fun getRealLayoutId(): Int {
return if (mUseAlternativeLayout) R.layout.fragment_game_collection_square_al else R.layout.fragment_game_collection_square
}
override fun onFragmentFirstVisible() {
super.onFragmentFirstVisible()
mViewModel.initData()
mViewModel.entrance = mEntrance
}
override fun onRealLayoutInflated(inflatedView: View) {
super.onRealLayoutInflated(inflatedView)
if (mUseAlternativeLayout) {
mAlternativeBinding = FragmentGameCollectionSquareAlBinding.bind(inflatedView)
} else {
mDefaultBinding = FragmentGameCollectionSquareBinding.bind(inflatedView)
}
}
override fun initRealView() {
super.initRealView()
mExposureListener = ExposureListener(this, mAdapter!!)
mListRv.addOnScrollListener(mExposureListener)
if (!mUseAlternativeLayout) {
initDefaultLayout()
} else {
initAlternativeLayout()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_SELECT_TAG) {
val tagName = data?.getStringExtra("tagName")
mDefaultBinding.tagTv.text = tagName ?: "标签筛选"
}
}
override fun provideListViewModel(): GameCollectionSquareViewModel {
mViewModel = viewModelProvider()
return mViewModel
}
override fun getItemDecoration() = VerticalItemDecoration(context, 16F, false, R.color.white)
override fun provideListAdapter(): ListAdapter<*> {
if (mAdapter == null) {
val basicExposureSource = arrayListOf<ExposureSource>().apply {
arguments?.getParcelable<ExposureSource>(EntranceUtils.KEY_EXPOSURE_SOURCE)?.let {
add(it)
}
add(ExposureSource("游戏单广场", ""))
}
mAdapter = GameCollectionSquareAdapter(requireContext(), mUseAlternativeLayout, mViewModel, basicExposureSource)
}
return mAdapter!!
}
override fun isAutomaticLoad() = false
private fun initDefaultLayout() {
// toolbar 消费 fitsSystemWindows 避免在 collapsingToolbar 下面出现多出来的 padding
// [https://stackoverflow.com/questions/48137666/viewgroup-inside-collapsingtoolbarlayout-show-extra-bottom-padding-when-set-fits]
ViewCompat.setOnApplyWindowInsetsListener(mDefaultBinding.appbar) { _, insets ->
(mDefaultBinding.toolbar.layoutParams as ViewGroup.MarginLayoutParams).topMargin = insets.systemWindowInsetTop
insets.consumeSystemWindowInsets()
}
val collapsingTrigger = 66F.dip2px() + DisplayUtils.getStatusBarHeight(context?.resources)
mDefaultBinding.toolbar.setNavigationOnClickListener { requireActivity().finish() }
mDefaultBinding.collapsingToolbar.scrimVisibleHeightTrigger = collapsingTrigger
mDefaultBinding.collapsingToolbar.scrimShownAction = {
DisplayUtils.setLightStatusBar(requireActivity(), it)
if (it) {
mDefaultBinding.titleTv.alpha = 1F
mDefaultBinding.titleTv.visibility = View.VISIBLE
mDefaultBinding.titleTv.setTextColor(R.color.black.toColor())
mDefaultBinding.toolbar.navigationIcon = R.drawable.ic_bar_back.toDrawable()
} else {
mDefaultBinding.titleTv.visibility = View.GONE
mDefaultBinding.toolbar.navigationIcon = R.drawable.ic_bar_back_light.toDrawable()
}
}
mDefaultBinding.titleTv.setOnClickListener {
if (ClickUtils.isFastDoubleClick(mDefaultBinding.titleTv.id, 300)) {
scrollToTop()
}
}
mDefaultBinding.appbar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, verticalOffset ->
val absOffset = abs(verticalOffset)
val invisibleOffset = DisplayUtils.dip2px(30F)
if (absOffset <= invisibleOffset) {
mDefaultBinding.titleTv.alpha = 1 - (absOffset.toFloat() / invisibleOffset)
} else {
mDefaultBinding.titleTv.alpha = 0F
}
mListRefresh?.isEnabled = absOffset <= 2
})
mDefaultBinding.orderRg.setOnCheckedChangeListener { _, checkedId ->
if (checkedId == R.id.hotRb) {
// 热门排序
// TODO 热门排序
} else {
// 最新排序
// TODO 最新排序
}
}
mDefaultBinding.fab.setOnClickListener {
// 创建游戏单
ifLogin(mEntrance) {
showRegulationTestDialogIfNeeded {
startActivity(GameCollectionEditActivity.getIntent(requireContext()))
}
}
}
mDefaultBinding.tagFilter.setOnClickListener {
startActivityForResult(GameCollectionTagSelectActivity.getIntent(requireContext(), true), REQUEST_SELECT_TAG)
}
mListRefresh?.setProgressViewOffset(false, 0, 118F.dip2px() + DisplayUtils.getStatusBarHeight(requireContext().resources))
// mSkeletonScreen = Skeleton.bind(mDefaultBinding?.skeletonPlaceholder).shimmer(false).load(R.layout.fragment_amway_skeleton).show()
}
private fun initAlternativeLayout() {
mAlternativeBinding.fab.setOnClickListener {
// 创建游戏单
ifLogin(mEntrance) {
showRegulationTestDialogIfNeeded {
startActivity(GameCollectionEditActivity.getIntent(requireContext()))
}
}
}
// mAlternativeBinding.listRv.addOnScrollListener(object : RecyclerView.OnScrollListener() {
// override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
// super.onScrolled(recyclerView, dx, dy)
// val scrollY = recyclerView.computeVerticalScrollOffset()
// }
// })
// mSkeletonScreen = Skeleton.bind(mAlternativeBinding?.skeletonPlaceholder).shimmer(false).load(
// R.layout.fragment_amway_skeleton_al).show()
}
override fun onFragmentPause() {
super.onPause()
DownloadManager.getInstance(context).removeObserver(dataWatcher)
// mElapsedHelper.pauseCounting()
}
override fun onResume() {
// if (isEverPause) mAdapter?.notifyDataSetChanged()
super.onResume()
}
override fun onFragmentResume() {
super.onFragmentResume()
DownloadManager.getInstance(context).addObserver(dataWatcher)
// mElapsedHelper.resetCounting()
// mElapsedHelper.resumeCounting()
}
override fun onLoadEmpty() {
super.onLoadEmpty()
if (!mUseAlternativeLayout) mDefaultBinding.tagFilterContainer.visibility = View.GONE
}
override fun onLoadDone() {
super.onLoadDone()
if (!mUseAlternativeLayout) mDefaultBinding.tagFilterContainer.visibility = View.VISIBLE
}
override fun onLoadError() {
super.onLoadError()
if (!mUseAlternativeLayout) mDefaultBinding.tagFilterContainer.visibility = View.GONE
}
override fun onLoadRefresh() {
super.onLoadRefresh()
if (!mUseAlternativeLayout) mDefaultBinding.tagFilterContainer.visibility = View.GONE
}
private fun scrollToTop() {
val firstItemPosition = mLayoutManager.findFirstVisibleItemPosition()
if (firstItemPosition >= 10) {
mListRv.scrollToPosition(6)
}
mListRv.smoothScrollToPosition(0)
mDefaultBinding.appbar.setExpanded(true)
}
//下载被删除事件
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEventMainThread(status: EBDownloadStatus) {
if ("delete" == status.status) {
// mAdapter?.notifyItemAndRemoveDownload(status)
}
}
//安装、卸载事件
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEventMainThread(busFour: EBPackage) {
// val data = mAdapter?.getGameEntityByPackage(busFour.packageName) ?: arrayListOf()
// for (gameAndPosition in data) {
// mAdapter?.notifyChildItem(gameAndPosition.position)
// }
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEventMainThread(reuse: EBReuse) {
if ("Refresh" == reuse.type) {
// mAdapter?.notifyDataSetChanged()
} else if (reuse.type == PersonalFragment.LOGIN_TAG) { // 登入
scrollToTop()
onRefresh()
// mViewModel.initData(false)
}
}
fun showUnzipFailureDialog(downloadEntity: DownloadEntity) {
// val data = mAdapter?.getGameEntityByPackage(downloadEntity.packageName) ?: return
// for (gameAndPosition in data) {
// val targetView = mLayoutManager.findViewByPosition(gameAndPosition.position)
// if (targetView != null) {
// if (targetView is RecyclerView) {
// // todo 如果时竖向专题该怎么判断?
// } else {
// DialogUtils.showUnzipFailureDialog(requireContext(), downloadEntity)
// return
// }
// }
// }
}
companion object {
const val REQUEST_SELECT_TAG = 100
}
}

View File

@ -0,0 +1,31 @@
package com.gh.gamecenter.gamecollection.square
import android.app.Application
import com.gh.gamecenter.baselist.ListViewModel
import com.gh.gamecenter.entity.GamesCollectionEntity
import com.gh.gamecenter.retrofit.RetrofitManager
import io.reactivex.Observable
import io.reactivex.Single
class GameCollectionSquareViewModel(application: Application) :
ListViewModel<GamesCollectionEntity, GamesCollectionEntity>(application) {
var entrance: String? = null
init {
initData()
}
fun initData() {
}
// override fun provideDataSingle(page: Int): Single<MutableList<GamesCollectionEntity>> = RetrofitManager.getInstance(getApplication())
// .api.getGameCollectionSquareList(page)
override fun mergeResultLiveData() {
mResultLiveData.addSource(mListLiveData) { mResultLiveData.postValue(it) }
}
override fun provideDataObservable(page: Int): Observable<MutableList<GamesCollectionEntity>>? = null
}

View File

@ -0,0 +1,144 @@
package com.gh.gamecenter.home.gamecollection
import android.view.View
import com.gh.common.util.DisplayUtils
import com.gh.common.view.stacklayoutmanager.StackAnimation
import com.gh.common.view.stacklayoutmanager.StackLayoutManager
import com.gh.gamecenter.R
import com.halo.assistant.HaloApp
import com.lightgame.utils.Utils
import kotlin.math.pow
class GameCollectionStackAnimation(
scrollOrientation: StackLayoutManager.ScrollOrientation,
visibleCount: Int
) : StackAnimation(scrollOrientation, visibleCount) {
private var mScale = 0.9F
private var mOutScale = 1.0F
private var mOutRotation = 0
/**
* 设置 item 缩放比例.
* @param scale 缩放比例
*/
fun setItemScaleRate(scale: Float) {
mScale = scale
}
/**
* 获取item缩放比例.
* @return item缩放比例
*/
fun getItemScaleRate(): Float {
return mScale
}
/**
* 设置 itemView 离开屏幕时候的缩放比例.
* @param scale 缩放比例
*/
fun setOutScale(scale: Float) {
mOutScale = scale
}
/**
* 获取 itemView 离开屏幕时候的缩放比例.
* @return 缩放比例
*/
fun getOutScale(): Float {
return mOutScale
}
/**
* 设置 itemView 离开屏幕时候的旋转角度.
* @param rotation 旋转角度
*/
fun setOutRotation(rotation: Int) {
mOutRotation = rotation
}
/**
* 获取 itemView 离开屏幕时候的旋转角度
* @return 旋转角度
*/
fun getOutRotation(): Int {
return mOutRotation
}
override fun doAnimation(firstMovePercent: Float, itemView: View, position: Int) {
val cover = itemView.findViewById<View>(R.id.cover)
val content = itemView.findViewById<View>(R.id.content)
val scale: Float
var alpha = 1.0F
val rotation: Float
if (position == 0) {
// 顶层item透明度变化
cover.alpha = 0F
content.alpha = 1F
if (firstMovePercent > 0.5) alpha = 1F - 2 * (firstMovePercent - 0.5F)
scale = 1 - ((1 - mOutScale) * firstMovePercent)
rotation = mOutRotation * firstMovePercent
} else {
val minScale = (mScale.toDouble().pow(position.toDouble())).toFloat()
val maxScale = (mScale.toDouble().pow((position - 1).toDouble())).toFloat()
scale = minScale + (maxScale - minScale) * firstMovePercent
when (position) {
1 -> {
cover.alpha = 0.3F - 0.3F * firstMovePercent
content.alpha = firstMovePercent
}
2 -> cover.alpha = 0.6F - 0.3F * firstMovePercent
3 -> {
cover.alpha = 1F - 0.4F * firstMovePercent
// 如果不改变会出现残留边框
// alpha = if (firstMovePercent == 0F) 0F else 1F
}
}
rotation = 0F
}
setItemPivotXY(mScrollOrientation, itemView)
rotationFirstVisibleItem(mScrollOrientation, itemView, rotation)
itemView.scaleX = scale
itemView.scaleY = scale
itemView.alpha = alpha
}
private fun setItemPivotXY(
scrollOrientation: StackLayoutManager.ScrollOrientation,
view: View
) {
when (scrollOrientation) {
StackLayoutManager.ScrollOrientation.RIGHT_TO_LEFT -> {
view.pivotX = view.measuredWidth.toFloat()
view.pivotY = view.measuredHeight.toFloat() / 2
}
StackLayoutManager.ScrollOrientation.LEFT_TO_RIGHT -> {
view.pivotX = 0F
view.pivotY = view.measuredHeight.toFloat() / 2
}
StackLayoutManager.ScrollOrientation.BOTTOM_TO_TOP -> {
view.pivotX = view.measuredWidth.toFloat() / 2
view.pivotY = view.measuredHeight.toFloat()
}
StackLayoutManager.ScrollOrientation.TOP_TO_BOTTOM -> {
view.pivotX = view.measuredWidth.toFloat() / 2
view.pivotY = 0F
}
}
}
private fun rotationFirstVisibleItem(
scrollOrientation: StackLayoutManager.ScrollOrientation,
view: View,
rotation: Float
) {
when (scrollOrientation) {
StackLayoutManager.ScrollOrientation.RIGHT_TO_LEFT -> view.rotationY = rotation
StackLayoutManager.ScrollOrientation.LEFT_TO_RIGHT -> view.rotationY = -rotation
StackLayoutManager.ScrollOrientation.BOTTOM_TO_TOP -> view.rotationX = -rotation
StackLayoutManager.ScrollOrientation.TOP_TO_BOTTOM -> view.rotationX = rotation
}
}
}

View File

@ -0,0 +1,110 @@
package com.gh.gamecenter.home.gamecollection
import android.view.View
import com.gh.common.util.dip2px
import com.gh.common.view.stacklayoutmanager.StackLayout
import com.gh.common.view.stacklayoutmanager.StackLayoutManager
class GameCollectionStackLayout(
scrollOrientation: StackLayoutManager.ScrollOrientation,
visibleCount: Int,
perItemOffset: Int
) : StackLayout(scrollOrientation, visibleCount, perItemOffset) {
private var mHasMeasureItemSize = false
private var mWidthSpace = 0
private var mHeightSpace = 0
private var mStartMargin = 0
private var mWidth = 0
private var mHeight = 0
private var mScrollOffset = 0
override fun doLayout(
stackLayoutManager: StackLayoutManager,
scrollOffset: Int,
firstMovePercent: Float,
itemView: View,
position: Int
) {
mWidth = stackLayoutManager.width
mHeight = stackLayoutManager.height
mScrollOffset = scrollOffset
if (!mHasMeasureItemSize) {
mWidthSpace =
mWidth - stackLayoutManager.getDecoratedMeasuredWidth(itemView) - 18F.dip2px()
mHeightSpace = mHeight - stackLayoutManager.getDecoratedMeasuredHeight(itemView)
mStartMargin = 16F.dip2px()
mHasMeasureItemSize = true
}
val left: Int
val top: Int
if (position == 0) {
left = getFirstVisibleItemLeft()
top = getFirstVisibleItemTop()
} else {
left = getAfterFirstVisibleItemLeft(position, firstMovePercent)
top = getAfterFirstVisibleItemTop(position, firstMovePercent)
}
val right = left + stackLayoutManager.getDecoratedMeasuredWidth(itemView)
val bottom = top + stackLayoutManager.getDecoratedMeasuredHeight(itemView)
stackLayoutManager.layoutDecorated(itemView, left, top, right, bottom)
}
override fun requestLayout() {
mHasMeasureItemSize = false //表示尺寸可能发生了改变
}
private fun getFirstVisibleItemLeft(): Int {
return when (mScrollOrientation) {
StackLayoutManager.ScrollOrientation.RIGHT_TO_LEFT -> mStartMargin - mScrollOffset % mWidth
StackLayoutManager.ScrollOrientation.LEFT_TO_RIGHT -> {
return if (mScrollOffset % mWidth == 0) {
mStartMargin
} else {
mStartMargin + (mWidth - mScrollOffset % mWidth)
}
}
else -> mWidthSpace / 2
}
}
private fun getFirstVisibleItemTop(): Int {
return when (mScrollOrientation) {
StackLayoutManager.ScrollOrientation.BOTTOM_TO_TOP -> mStartMargin - mScrollOffset % mHeight
StackLayoutManager.ScrollOrientation.TOP_TO_BOTTOM -> {
return if (mScrollOffset % mHeight == 0) {
mStartMargin
} else {
mStartMargin + (mHeight - mScrollOffset % mHeight)
}
}
else -> mHeightSpace / 2
}
}
private fun getAfterFirstVisibleItemLeft(visiblePosition: Int, movePercent: Float): Int {
return when (mScrollOrientation) {
StackLayoutManager.ScrollOrientation.RIGHT_TO_LEFT -> (mStartMargin + mPerItemOffset * (visiblePosition - movePercent)).toInt()
StackLayoutManager.ScrollOrientation.LEFT_TO_RIGHT -> (mStartMargin - mPerItemOffset * (visiblePosition - movePercent)).toInt()
else -> mWidthSpace / 2
}
}
private fun getAfterFirstVisibleItemTop(visiblePosition: Int, movePercent: Float): Int {
return when (mScrollOrientation) {
StackLayoutManager.ScrollOrientation.BOTTOM_TO_TOP -> (mStartMargin + mPerItemOffset * (visiblePosition - movePercent)).toInt()
StackLayoutManager.ScrollOrientation.TOP_TO_BOTTOM -> (mStartMargin - mPerItemOffset * (visiblePosition - movePercent)).toInt()
else -> mHeightSpace / 2
}
}
private fun getStartMargin(): Int {
return when (mScrollOrientation) {
StackLayoutManager.ScrollOrientation.RIGHT_TO_LEFT, StackLayoutManager.ScrollOrientation.LEFT_TO_RIGHT -> mWidthSpace / 2
else -> mHeightSpace / 2
}
}
}

View File

@ -0,0 +1,65 @@
package com.gh.gamecenter.home.gamecollection
import android.content.Context
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.gh.gamecenter.R
import com.gh.gamecenter.databinding.HomeGameCollectionCardItemBinding
import com.gh.gamecenter.entity.GamesCollectionEntity
import com.lightgame.adapter.BaseRecyclerAdapter
class HomeGameCollectionAdapter(context: Context) :
BaseRecyclerAdapter<RecyclerView.ViewHolder>(context) {
private var mGameCollectionList = ArrayList<GamesCollectionEntity>()
fun setGameCollectionList(gameCollectionList: ArrayList<GamesCollectionEntity>) {
mGameCollectionList = gameCollectionList
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = HomeGameCollectionCardViewHolder(
HomeGameCollectionCardItemBinding.bind(
mLayoutInflater.inflate(
R.layout.home_game_collection_card_item,
parent,
false
)
)
)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder is HomeGameCollectionCardViewHolder) {
var realPosition: Int
when (position) {
0 -> {
realPosition = getRealCount() - 2
}
1 -> {
realPosition = getRealCount() - 1
}
itemCount - 2 -> {
realPosition = 0
}
itemCount - 1 -> {
realPosition = 1
}
else -> {
realPosition = position - 2
}
}
holder.bindGameListCard(position, mGameCollectionList[realPosition])
}
}
override fun getItemCount() = getRealCount() + 4
fun getRealCount() = mGameCollectionList.size
class HomeGameCollectionCardViewHolder(val binding: HomeGameCollectionCardItemBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bindGameListCard(itemPosition: Int, entity: GamesCollectionEntity) {
binding.entity = entity
}
}
}

View File

@ -0,0 +1,46 @@
package com.gh.gamecenter.home.gamecollection
import com.gh.base.BaseRecyclerViewHolder
import com.gh.common.util.dip2px
import com.gh.common.view.stacklayoutmanager.StackLayoutManager
import com.gh.gamecenter.databinding.HomeGameCollectionItemBinding
import com.gh.gamecenter.entity.GamesCollectionEntity
class HomeGameCollectionViewHolder(val binding: HomeGameCollectionItemBinding) :
BaseRecyclerViewHolder<Any>(binding.root) {
fun bindGameCollectionList(gameCollectionList: ArrayList<GamesCollectionEntity>) {
if (binding.recyclerView.adapter is HomeGameCollectionAdapter) {
return
}
val adapter = HomeGameCollectionAdapter(binding.root.context)
val manager = StackLayoutManager(
StackLayoutManager.ScrollOrientation.RIGHT_TO_LEFT,
3,
GameCollectionStackAnimation::class.java,
GameCollectionStackLayout::class.java
)
manager.setItemOffset(9F.dip2px())
manager.setItemChangedListener(object : StackLayoutManager.ItemChangedListener {
override fun onItemChanged(position: Int) {
when (position) {
0 -> binding.recyclerView.scrollToPosition(adapter.itemCount - 3)
// 最后一个item跳转
adapter.itemCount - 3 -> binding.recyclerView.scrollToPosition(1)
}
}
})
binding.recyclerView.layoutManager = manager
binding.recyclerView.adapter = adapter
binding.recyclerView.isNestedScrollingEnabled = false
adapter.setGameCollectionList(gameCollectionList)
binding.recyclerView.post {
// 定位到实际的第一个item
manager.scrollToPosition(FIRST_ITEM_POSITION)
}
}
companion object {
const val FIRST_ITEM_POSITION = 2
}
}