Android imitation Taobao, Jingdong Banner swipe to view graphic details

I am participating in the "Nuggets · Sailing Program"

write in front

This article is based ViewPager2on the Bannereffect achieved, and then realizes the effect of imitating Taobao and Jingdong Bannerwhen swiping to the last page and continuing to swipe to view the details of the pictures and texts. About ViewPager2the principle and its packaging, you can refer to the previous two articles:
1. Android in-depth understanding of ViewPager2 principle and practice (Part 1)
2. Android in-depth understanding of ViewPager2 principle and practice (Part 2)

renderings

Banner Swipe to view graphic details

Principle analysis

Swipe to see more

  • BannerThe one on the right 查看更多Viewis wrapped, the default 子Viewwidth is , and the one on the right side of the screen is invisible;父ViewBannermatch_parent查看更多
  • When Bannersliding left and right, the current sliding event is Bannerconsumed in , that is, it 父Viewwill not be intercepted.
  • When you Bannerswipe to the far right and continue to swipe, 父Viewthe event will be intercepted at this time, so that the event will be 父Viewtaken over and the 父Viewevent onTouchEvent()will be consumed in , 父Viewand the content in the swipe can be swiped. How to slide it? During the MOVEevent, by scrollTo()/scrollBy()sliding, and during the UP/CANCELevent, you need to Scrollerautomatically startScroll()slide to 子Viewthe left or right to view more, so as to complete the consumption of an event;
  • When the UP/CANCELevent is triggered, 子Viewthe distance to view more swipes is more than half, and it is considered that more operations need to be triggered to view more. Of course, the values ​​here can be set by yourself.

core code

  • TJBannerFragment.kt
/**
 * 仿淘宝京东宝贝详情Fragment
 */
class TJBannerFragment : BaseFragment() {
    private val mModels: MutableList<Any> = 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 (parent 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<Any>, 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
        }
    }
}
复制代码

父ViewThe comments are very clear, so there is no need to explain too much. Here is one thing to pay attention to. It is known that Bannerwhen sliding on the last page, you need to judge the sliding direction: if you continue to slide to the left, you need to 父Viewintercept the sliding event and consume it yourself; when you slide to the right, 父Viewdo not Sliding events need to be processed, Bannerand event consumption is still performed.

However 滑动方向需要起始位置(DOWN事件)的X坐标 - 滑动时的X坐标(MOVE事件) 的差值进行判断, where does the problem take the X coordinate of the starting position? 父ViewAre you onInterceptTouchEvent()->DOWN事件there? This is not possible, because the sliding direction is MOVE事件judged in the in, and 父Viewif onInterceptTouchEvent()->DOWN事件it is intercepted in the in, the subsequent events will not Bannerbe passed in. Here you can choose 父Viewto dispatchTouchEvent()->DOWN事件solve it.

The XML layout corresponding to VpLoadMoreView :

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/white"
    android:orientation="horizontal"
    tools:parentTag="android.widget.LinearLayout">

    <!--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>
</merge>
复制代码

Here , and must 父View(VpLoadMoreView)be LinearLayouta horizontal layout, XMLthe label used in the top-level layout merge, which can not only optimize the layout of one layer, but also 父Viewdirectly operate and load the graphic details in it 子View.

Source address

For the complete code address, see: Android imitation Taobao, Jingdong Banner swipe to the end to view the graphic details

Guess you like

Origin juejin.im/post/7156059973728862238