Android仿淘寶、京東Banner滑動檢視圖文詳情
我正在參加「掘金·啟航計劃」
寫在前面
本文基於 ViewPager2
實現的 Banner
效果,進而實現了仿淘寶、京東Banner
滑動至最後一頁時繼續滑動來檢視圖文詳情的效果。關於 ViewPager2
的原理及其封裝,可以參見之前的兩篇文章:
1、Android 深入理解ViewPager2原理及其實踐(上篇)
2、Android 深入理解ViewPager2原理及其實踐(下篇)
效果圖
原理分析
- Banner
與右側的檢視更多View
都是子View
,被父View
包裹,預設Banner
的寬度是match_parent
,而檢視更多
則是在螢幕的右側,處於不可見狀態;
- 當Banner
進行左右滑動時,當前的滑動事件是在Banner
中消費的,即父View
不會進行攔截。
- 當Banner
滑動到最右側且要繼續滑動時,此時父View
會進行事件的攔截,從而事件由父View
接管,並在父View
的onTouchEvent()
中消費事件,此時就可以滑動父View
中的內容了。怎麼滑動呢?在MOVE
事件時通過scrollTo()/scrollBy()
滑動,而在UP/CANCEL
事件時,需要通過Scroller
的startScroll()
自動滑動到檢視更多子View
的左側或右側,從而完成一次事件的消費;
- 當UP/CANCEL
事件觸發時,檢視更多子View
滑動的距離超過一半,認為需要觸發檢視更多操作了,當然這裡的值都可以自行設定。
核心程式碼
- TJBannerFragment.kt ``` /**
-
仿淘寶京東寶貝詳情Fragment */ class TJBannerFragment : BaseFragment() { private val mModels: MutableList
= mutableListOf() private val mContainer: VpLoadMoreView by id(R.id.vp2_load_more) override fun getLayoutId(): Int { return R.layout.fragment_tx_news_n }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { initVerticalTxScroll() }
private fun initVerticalTxScroll() { mModels.add(TxNewsModel(MConstant.IMG_4, "美輪美奐節目", "奧運五環緩緩升起")) mModels.add(TxNewsModel(MConstant.IMG_1, "精美商品", "9塊9包郵")) mContainer.setData(mModels) { showToast("開啟更多頁面") } } }
- **VpLoadMoreView.kt(父View)**
class VpLoadMoreView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyle: Int = 0, ) : LinearLayout(context, attrs, defStyle) {private val mMVPager2: MVPager2 by id(R.id.mvp_pager2) private var mNeedIntercept: Boolean = false //是否需要攔截VP2事件 private val mLoadMoreContainer: LinearLayout by id(R.id.load_more_container) private val mIvArrow: ImageView by id(R.id.iv_pull) private val mTvTips: TextView by id(R.id.tv_tips)
private var mCurPos: Int = 0 //Banner當前滑動的位置 private var mLastX = 0f private var mLastDownX = 0f //用於判斷滑動方向 private var mMenuWidth = 0 //載入更多View的寬度 private var mShowMoreMenuWidth = 0 //載入更多發生變化時的寬度 private var mLastStatus = false // 預設箭頭樣式 private var mAction: (() -> Unit)? = null private var mScroller: OverScroller private var isTouchLeft = false //是否是向左滑動 private var animRightStart = RotateAnimation(0f, -180f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f).apply { duration = 300 fillAfter = true }
private var animRightEnd = RotateAnimation(-180f, 0f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f).apply { duration = 300 fillAfter = true }
init { orientation = HORIZONTAL View.inflate(context, R.layout.fragment_tx_news, this) mScroller = OverScroller(context) }
/* * @param mModels 要載入的資料 * @param action 回撥Action / fun setData(mModels: MutableList
, action: () -> Unit) { this.mAction = action mMVPager2.setModels(mModels) .setLoop(false) //非迴圈模式 .setIndicatorShow(false) .setLoader(TxNewsLoader(mModels)) .setPageTransformer(CompositePageTransformer().apply { addTransformer(MarginPageTransformer(15)) }) .setOrientation(ViewPager2.ORIENTATION_HORIZONTAL) .setAutoPlay(false) .setOnBannerClickListener(object : OnBannerClickListener { override fun onItemClick(position: Int) { showToast(mModels[position].toString()) } }) .registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { override fun onPageScrollStateChanged(state: Int) { if (mCurPos == mModels.lastIndex && isTouchLeft && state == ViewPager2.SCROLL_STATE_DRAGGING) { //Banner在最後一頁 & 手勢往左滑動 & 當前是滑動狀態 mNeedIntercept = true //父View可以攔截 mMVPager2.setUserInputEnabled(false) //VP2設定為不可滑動 } } override fun onPageSelected(position: Int) { mCurPos = position } }) .start()
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { mMenuWidth = mLoadMoreContainer.measuredWidth mShowMoreMenuWidth = mMenuWidth / 3 * 2 super.onLayout(changed, l, t, r, b) }
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { when (ev?.action) { MotionEvent.ACTION_DOWN -> { mLastX = ev.x mLastDownX = ev.x } MotionEvent.ACTION_MOVE -> { isTouchLeft = mLastDownX - ev.x > 0 //判斷滑動方向 } } return super.dispatchTouchEvent(ev) }
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { var isIntercept = false when (ev?.action) { MotionEvent.ACTION_MOVE -> isIntercept = mNeedIntercept //是否攔截Move事件 } //log("ev?.action: ${ev?.action},isIntercept: $isIntercept") return isIntercept }
@SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(ev: MotionEvent?): Boolean { when (ev?.action) { MotionEvent.ACTION_MOVE -> { val mDeltaX = mLastX - ev.x if (mDeltaX > 0) { //向左滑動 if (mDeltaX >= mMenuWidth || scrollX + mDeltaX >= mMenuWidth) { //右邊緣檢測 scrollTo(mMenuWidth, 0) return super.onTouchEvent(ev) } } else if (mDeltaX < 0) { //向右滑動 if (scrollX + mDeltaX <= 0) { //左邊緣檢測 scrollTo(0, 0) return super.onTouchEvent(ev) } } showLoadMoreAnim(scrollX + mDeltaX) scrollBy(mDeltaX.toInt(), 0) mLastX = ev.x } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { smoothCloseMenu() mNeedIntercept = false mMVPager2.setUserInputEnabled(true) //執行回撥 val mDeltaX = mLastX - ev.x if (scrollX + mDeltaX >= mShowMoreMenuWidth) { mAction?.invoke() } } } return super.onTouchEvent(ev) }
private fun smoothCloseMenu() { mScroller.forceFinished(true) /* * 左上為正,右下為負 * startX:X軸開始位置 * startY: Y軸結束位置 * dx:X軸滑動距離 * dy:Y軸滑動距離 * duration:滑動時間 / mScroller.startScroll(scrollX, 0, -scrollX, 0, 300) invalidate() }
override fun computeScroll() { if (mScroller.computeScrollOffset()) { showLoadMoreAnim(0f) //動畫還原 scrollTo(mScroller.currX, mScroller.currY) invalidate() } }
private fun showLoadMoreAnim(dx: Float) { val showLoadMore = dx >= mShowMoreMenuWidth if (mLastStatus == showLoadMore) return if (showLoadMore) { mIvArrow.startAnimation(animRightStart) mTvTips.text = "釋放檢視圖文詳情" mLastStatus = true } else { mIvArrow.startAnimation(animRightEnd) mTvTips.text = "滑動檢視圖文詳情" mLastStatus = false } } }
``
父View的註釋很清晰,不用過多解釋了,這裡需要注意一點,已知在
Banner的最後一頁滑動時需要判斷滑動方向:繼續向左滑動,需要
父View攔截滑動事件並自己進行消費;向右滑動時,
父View不需要處理滑動事件,仍由
Banner`進行事件消費。
而滑動方向需要起始位置(DOWN事件)的X座標 - 滑動時的X座標(MOVE事件) 的差值進行判斷
,那問題在哪裡取起始位置的X座標呢?在父View
的onInterceptTouchEvent()->DOWN事件
裡嗎?這裡是不行的,因為滑動方向是在MOVE事件
裡判斷的,在父View
的onInterceptTouchEvent()->DOWN事件
裡攔截的話,後續事件不會往Banner
裡傳遞了。這裡可以選擇在父View
的dispatchTouchEvent()->DOWN事件
裡即可解決。
VpLoadMoreView對應的XML佈局: ```
<!--ViewPager2-->
<org.ninetripods.lib_viewpager2.MVPager2
android:id="@+id/mvp_pager2"
android:layout_width="match_parent"
android:layout_height="200dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!--載入更多View-->
<LinearLayout
android:id="@+id/load_more_container"
android:layout_width="100dp"
android:layout_height="200dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ImageView
android:id="@+id/iv_pull"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:src="@drawable/icon_arrow_pull" />
<TextView
android:id="@+id/tv_tips"
android:layout_width="16dp"
android:layout_height="match_parent"
android:layout_marginStart="10dp"
android:gravity="center_vertical"
android:text="滑動檢視圖文詳情"
android:textColor="#333333"
android:textSize="14sp"
android:textStyle="bold" />
</LinearLayout>
``
這裡的
父View(VpLoadMoreView)是
LinearLayout,且必須是橫向佈局,
XML的頂層佈局使用的
merge標籤,這樣既可以優化一層佈局,又可以在
父View中直接操作載入圖文詳情的
子View`。
原始碼地址
完整程式碼地址參見:Android仿淘寶、京東Banner滑動至最後檢視圖文詳情
- 一起來體驗一把ChatGPT
- Android仿淘寶、京東Banner滑動檢視圖文詳情
- 深入理解Kotlin協程
- Kotlin常用Collection集合操作整理
- Android 效能優化篇之SharedPreferences使用優化
- Kotlin 作用域函式之let、with、run、also、apply的使用筆記
- Android Jetpack系列之MVVM使用及封裝(續)
- Kotlin行內函數inline、noinline、crossinline
- Android Jetpack系列之DataStore
- JUC系列學習:阻塞佇列BlockingQueue介紹及其相關實現ArrayBlockingQueue、LinkedBlockingQueue等的使用及原始碼分析
- JUC系列學習:ReentrantLock的使用、原始碼解析及與Synchronized的異同
- Android Jetpack系列之ViewModel
- Android Jetpack系列之LiveData
- Android Jetpack系列之Lifecycle
- Android 基於Jetpack LiveData實現訊息匯流排
- Android Jetpack系列之MVVM使用及封裝