ViewPager和其他View联动动画

  • 实现效果

    图中的灰色区域都是可滑动区域,其中我用三种文本颜色表示区域中三个不同的View,黑色部分是TextView,蓝色部分是LinearLayout,红色部分是ViewPager内部的内容。

  • 技术难点

    首先要禁用ViewPager的随手指拖动,手指滑动如果抬起时没有一个fling的势头则不会触发动画效果;

    其次点击灰色区域的手势监听实现滚动的同时,不能影响ViewPager内容的点击事件,也就是点击红色区域还会触发点击事件;

    最后就是这三部分的动画是联动的。

  • 布局和代码

    <?xml version="1.0" encoding="utf-8"?>
    <androidx.constraintlayout.widget.ConstraintLayout 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:paddingTop="15dp"
        android:paddingBottom="15dp"
        android:background="@android:color/darker_gray"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        >
    ​
        <TextView
            android:id="@+id/airport"
            android:layout_width="0dp"
            android:layout_weight="1"
            android:layout_marginStart="23dp"
            android:layout_height="wrap_content"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:text="无题"
            android:textSize="20sp"
            android:textColor="@android:color/black"
            />
    ​
        <LinearLayout
            android:id="@+id/ll_title"
            android:layout_marginTop="15dp"
            android:layout_marginStart="23dp"
            android:layout_marginEnd="20dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/airport"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal"
            >
    ​
            <TextView
                android:layout_width="0dp"
                android:layout_weight="1"
                android:layout_height="wrap_content"
                android:text="姓名"
                android:textSize="12sp"
                android:textColor="@android:color/holo_blue_dark"
                />
    ​
            <TextView
                android:layout_width="0dp"
                android:layout_weight="1"
                android:layout_height="wrap_content"
                android:text="性别"
                android:textSize="12sp"
                android:textColor="@android:color/holo_blue_dark"
                />
    ​
            <TextView
                android:layout_width="0dp"
                android:layout_weight="1"
                android:layout_height="wrap_content"
                android:text="年龄"
                android:textSize="12sp"
                android:textColor="@android:color/holo_blue_dark"
                />
    ​
            <TextView
                android:layout_width="0dp"
                android:layout_weight="1"
                android:layout_height="wrap_content"
                android:text="身高"
                android:textSize="12sp"
                android:textColor="@android:color/holo_blue_dark"
                />
    ​
            <TextView
                android:layout_width="0dp"
                android:layout_weight="1"
                android:layout_height="wrap_content"
                android:text="臂展"
                android:textSize="12sp"
                android:textColor="@android:color/holo_blue_dark"
                />
    ​
        </LinearLayout>
    ​
        <com.mph.jetpackproj.cc_demo.view_pager.NoScrollViewPager
            android:id="@+id/flightDataPanel"
            android:layout_marginStart="23dp"
            android:layout_marginEnd="20dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/ll_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            />
    ​
        <LinearLayout
            android:id="@+id/indicator"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/flightDataPanel"
            app:layout_constraintEnd_toEndOf="parent"
            android:orientation="horizontal"
            android:padding="5dp"
            >
    ​
        </LinearLayout>
    ​
    </androidx.constraintlayout.widget.ConstraintLayout>

    airport对应黑色部分View,ll_title对应蓝色部分View,flightDataPanel对应红色部分ViewPager。

    NoScrollViewPager继承自ViewPager:

    /**
     *
     * @author mph
     * @date 2020/9/24
     */
    class NoScrollViewPager(context: Context, attrs: AttributeSet?) : ViewPager(context, attrs) {
    ​
        private var beforeX = 0f
        private var lastX = 0f
    ​
        companion object {
            private const val FLING_MIN_VELOCITY = 100
        }
    ​
        private lateinit var mVelocityTracker: VelocityTracker
    ​
        /**
         * wrap_content高度
         */
        override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
            var newHeightMeasureSpec = heightMeasureSpec
            var height = 0
            for (i in 0 until childCount) {
                val child: View = getChildAt(i)
                child.measure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED))
                val h: Int = child.measuredHeight
                if (h > height) height = h
            }
            newHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)
            super.onMeasure(widthMeasureSpec, newHeightMeasureSpec)
        }
    ​
        /**
         * 禁止滑动
         */
        override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
            when (ev.action) {
                MotionEvent.ACTION_DOWN -> {
                    mVelocityTracker = VelocityTracker.obtain()
                    mVelocityTracker.addMovement(ev)
                    beforeX = ev.x
                }
                MotionEvent.ACTION_MOVE -> {
                    mVelocityTracker.addMovement(ev)
                }
                MotionEvent.ACTION_UP -> {
                    lastX = ev.x
                    mVelocityTracker.addMovement(ev)
                    //最近500毫秒内的速度
                    mVelocityTracker.computeCurrentVelocity(500)
                    val velocityX = abs(mVelocityTracker.xVelocity)
                    mVelocityTracker.clear()
                    mVelocityTracker.recycle()
                    return when {
                        beforeX == lastX -> { // 点击事件
                            super.onInterceptTouchEvent(ev)
                        }
                        velocityX > FLING_MIN_VELOCITY -> {
                            if (beforeX > lastX) { // 向左滑动
                                listener?.onLeftFling()
                            } else if (beforeX < lastX) { // 向右滑动
                                listener?.onRightFling()
                            }
                            beforeX = 0f
                            lastX = 0f
                            true
                        }
                        else -> {
                            true
                        }
                    }
                }
            }
            return false
        }
    ​
        interface FlingListener {
            fun onLeftFling()
            fun onRightFling()
        }
    ​
        private var listener: FlingListener? = null
    ​
        fun setFlingListener(listener: FlingListener?) {
            this.listener = listener
        }
    ​
    }

    VelocityTracker是用来检测手势滑动速度的,在ACTION_DOWN的时候obtain,并且在每个事件中都要addMovement,最后在ACTION_UP时通过computeCurrentVelocity(500)计算最近500毫秒内的速度,也就是松开手指时的速度,然后调用mVelocityTracker.xVelocity获取x轴上的速度,这里我判断大于100则属于onFling动作beforeX 和lastX大小来判断是左滑还是右滑,最后记得clear和recycle。同时注意这里并没有消费掉点击事件。

    ViewPager部分解决了再来看整个的灰色区域的View封装:

    /**
     *
     * @author mph
     * @date 2020/9/24
     */
    class AirportBigScreen @JvmOverloads constructor(
        context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
    ) : LinearLayout(context, attrs, defStyleAttr), GestureDetector.OnGestureListener,
        View.OnTouchListener {
    ​
        companion object {
            //左滑/右滑
            private const val LEFT_SCROLL = 0
            private const val RIGHT_SCROLL = 1
    ​
            //动画
            private const val DELAY_TIME_DURATION = 180L
            private const val ANIM_DURATION = 200L
    ​
        }
    ​
        private var mDetector: GestureDetector = GestureDetector(context, this)
    ​
        private val mRootView =
            LayoutInflater.from(context).inflate(R.layout.view_airport_big_screen, this)
    ​
        private var mData: ArrayList<BigScreenResponse.Data.AirportDataItem> = arrayListOf()
    ​
        private var mViewPager: NoScrollViewPager
    ​
        private var mAdapter: MyPagerAdapter
    ​
        private var mIndicator: LinearLayout
    ​
        private var mAirport: TextView
    ​
        private var mTitlePanel: LinearLayout
    ​
        private var mAnimSet: AnimatorSet? = null
    ​
        init {
            setOnTouchListener(this)
            mViewPager = mRootView.findViewById(R.id.flightDataPanel)
            mViewPager.setFlingListener(object : NoScrollViewPager.FlingListener {
                override fun onLeftFling() {
    //                Toast.makeText(context, "向左滑", Toast.LENGTH_SHORT).show()
                    switchPager(mViewPager.currentItem + 1, LEFT_SCROLL)
                }
    ​
                override fun onRightFling() {
    //                Toast.makeText(context, "向右滑", Toast.LENGTH_SHORT).show()
                    switchPager(mViewPager.currentItem - 1, RIGHT_SCROLL)
                }
            })
            mAdapter = MyPagerAdapter(context)
            mViewPager.adapter = mAdapter
            mIndicator = mRootView.findViewById(R.id.indicator)
            mAirport = mRootView.findViewById(R.id.airport)
            mTitlePanel = mRootView.findViewById(R.id.ll_title)
        }
    ​
        fun setData(data: List<BigScreenResponse.Data.AirportDataItem>) {
            mData.clear()
            mData.addAll(data)
            mAdapter.notifyDataSetChanged()
    ​
            //指示标记
            initIndicator()
    ​
        }
    ​
        private fun initIndicator() {
            mIndicator.removeAllViews()
            //只有一个机场的时候不显示下面的切换提示View
            if (mData.size > 1) {
                for (i in mData?.indices ?: IntRange(0, 0)) {
                    val item = View(context)
                    val lp: LinearLayout.LayoutParams = LinearLayout.LayoutParams(
                        dip2px(context, 8f),
                        dip2px(context, 2f)
                    )
                    lp.leftMargin = dip2px(context, 2f)
                    lp.rightMargin = dip2px(context, 2f)
                    item.layoutParams = lp
                    item.setBackgroundResource(R.drawable.module_main_selector_indicator_item_back)
                    item.isSelected = false
                    mIndicator.addView(item)
                }
                if (null != mIndicator.getChildAt(mViewPager.currentItem)) {
                    mIndicator.getChildAt(mViewPager.currentItem).isSelected = true
                }
            }
    ​
            mAirport.text = mData[0].airportName
        }
    ​
        /**
         * @param index viewpager新标签下标
         * @param flag  触发事件是左滑还是右滑
         */
        private fun switchPager(index: Int, flag: Int) {
            //如果合法范围内
            if (index >= 0 && index < mData.size) {
                //触发动画
                when (flag) {
                    LEFT_SCROLL -> startAnimOnLeftScroll(index)
                    RIGHT_SCROLL -> startAnimOnRightScroll(index)
                }
            }
        }
    ​
        /**
         * 左滑
         */
        private fun startAnimOnLeftScroll(index: Int) {
            onLeftOut(index)
        }
    ​
        /**
         * 右滑
         */
        private fun startAnimOnRightScroll(index: Int) {
            onRightOut(index)
        }
    ​
        /**
         * 左边滑出
         */
        private fun onLeftOut(index: Int) {
            mAnimSet = AnimatorSet()
            val animator0 = ObjectAnimator.ofFloat(
                mAirport,
                "translationX",
                -(mAirport.width + mAirport.marginStart).toFloat()
            )
            val animator1 = ObjectAnimator.ofFloat(
                mTitlePanel,
                "translationX",
                -(mTitlePanel.width + mTitlePanel.marginStart).toFloat()
            )
            val animator2 = ObjectAnimator.ofFloat(
                mViewPager,
                "translationX",
                -(mViewPager.width + mViewPager.marginStart).toFloat()
            )
    ​
            animator1.startDelay = DELAY_TIME_DURATION
            animator2.startDelay = DELAY_TIME_DURATION
    ​
            mAnimSet?.duration = ANIM_DURATION
            mAnimSet?.interpolator = LinearInterpolator()
            mAnimSet?.playTogether(
                animator0,
                animator1,
                animator2
            )
            mAnimSet?.addListener(object : Animator.AnimatorListener {
                override fun onAnimationRepeat(animation: Animator?) {
                }
    ​
                override fun onAnimationEnd(animation: Animator?) {
                    //动画结束切换viewpager
                    mViewPager.setCurrentItem(index, false)
                    /**更新机场名字*/
                    val info: BigScreenResponse.Data.AirportDataItem = mData[mViewPager.currentItem]
                    mAirport.text = info.airportName
    ​
                    mAirport.translationX = getScreenWidth(context).toFloat()
                    mTitlePanel.translationX = getScreenWidth(context).toFloat()
                    mViewPager.translationX = getScreenWidth(context).toFloat()
    ​
                    onRightIn()
                }
    ​
                override fun onAnimationCancel(animation: Animator?) {
                }
    ​
                override fun onAnimationStart(animation: Animator?) {
                }
    ​
            })
            mAnimSet?.start()
        }
    ​
        /**
         * 右边滑入
         */
        private fun onRightIn() {
            mAnimSet = AnimatorSet()
            val animator0 = ObjectAnimator.ofFloat(
                mAirport,
                "translationX",
                mRootView.left.toFloat()
            )
            val animator1 = ObjectAnimator.ofFloat(
                mTitlePanel,
                "translationX",
                mRootView.left.toFloat()
            )
            val animator2 = ObjectAnimator.ofFloat(
                mViewPager,
                "translationX",
                mRootView.left.toFloat()
            )
    ​
            animator1.startDelay = 150
            animator2.startDelay = 150
    ​
            mAnimSet?.duration = 300
            mAnimSet?.interpolator = LinearInterpolator()
            mAnimSet?.playTogether(
                animator0,
                animator1,
                animator2
            )
            mAnimSet?.addListener(object : Animator.AnimatorListener {
                override fun onAnimationRepeat(animation: Animator?) {
                }
    ​
                override fun onAnimationEnd(animation: Animator?) {
                    //更新指示条
                    updateIndicators()
                }
    ​
                override fun onAnimationCancel(animation: Animator?) {
                }
    ​
                override fun onAnimationStart(animation: Animator?) {
                }
    ​
            })
            mAnimSet?.start()
        }
    ​
        /**
         * 右边滑出
         */
        private fun onRightOut(index: Int) {
            mAnimSet = AnimatorSet()
            val animator0 = ObjectAnimator.ofFloat(
                mAirport,
                "translationX",
                getScreenWidth(context).toFloat()
            )
            val animator1 = ObjectAnimator.ofFloat(
                mTitlePanel,
                "translationX",
                getScreenWidth(context).toFloat()
            )
            val animator2 = ObjectAnimator.ofFloat(
                mViewPager,
                "translationX",
                getScreenWidth(context).toFloat()
            )
    ​
            animator0.startDelay = DELAY_TIME_DURATION
    ​
            mAnimSet?.duration = ANIM_DURATION
            mAnimSet?.interpolator = LinearInterpolator()
            mAnimSet?.playTogether(
                animator0,
                animator1,
                animator2
            )
            mAnimSet?.addListener(object : Animator.AnimatorListener {
                override fun onAnimationRepeat(animation: Animator?) {
                }
    ​
                override fun onAnimationEnd(animation: Animator?) {
                    //动画结束切换viewpager
                    mViewPager.setCurrentItem(index, false)
                    /**更新机场名字*/
                    val info: BigScreenResponse.Data.AirportDataItem = mData[mViewPager.currentItem]
                    mAirport.text = info.airportName
    ​
                    mAirport.translationX = -(mAirport.width + mAirport.marginStart).toFloat()
                    mTitlePanel.translationX = -(mTitlePanel.width + mTitlePanel.marginStart).toFloat()
                    mViewPager.translationX = -(mViewPager.width + mViewPager.marginStart).toFloat()
    ​
                    onLeftIn()
                }
    ​
                override fun onAnimationCancel(animation: Animator?) {
                }
    ​
                override fun onAnimationStart(animation: Animator?) {
                }
    ​
            })
            mAnimSet?.start()
        }
    ​
        /**
         * 左边滑入
         */
        private fun onLeftIn() {
            mAnimSet = AnimatorSet()
            val animator0 = ObjectAnimator.ofFloat(
                mAirport,
                "translationX",
                (mRootView.left).toFloat()
            )
            val animator1 = ObjectAnimator.ofFloat(
                mTitlePanel,
                "translationX",
                (mRootView.left).toFloat()
            )
            val animator2 = ObjectAnimator.ofFloat(
                mViewPager,
                "translationX",
                (mRootView.left).toFloat()
            )
    ​
            animator0.startDelay = DELAY_TIME_DURATION
    ​
            mAnimSet?.duration = ANIM_DURATION
            mAnimSet?.interpolator = LinearInterpolator()
            mAnimSet?.playTogether(
                animator0,
                animator1,
                animator2
            )
            mAnimSet?.addListener(object : Animator.AnimatorListener {
                override fun onAnimationRepeat(animation: Animator?) {
                }
    ​
                override fun onAnimationEnd(animation: Animator?) {
                    //更新指示条
                    updateIndicators()
                }
    ​
                override fun onAnimationCancel(animation: Animator?) {
                }
    ​
                override fun onAnimationStart(animation: Animator?) {
                }
    ​
            })
            mAnimSet?.start()
        }
    ​
        /**
         * @desc 更新indicator
         */
        private fun updateIndicators() {
    ​
            //只有一个机场的时候不显示下面的切换提示View
            if (mData.size > 1) {
                for (i in mData.indices) {
                    mIndicator.getChildAt(i).isSelected = false
                }
                mIndicator.getChildAt(mViewPager.currentItem).isSelected = true
            }
        }
    ​
        inner class MyPagerAdapter(context: Context) : PagerAdapter() {
    ​
            private var mContext = context
    ​
            override fun isViewFromObject(view: View, `object`: Any): Boolean {
                return view == `object`
            }
    ​
            override fun getCount(): Int {
                return mData.size
            }
    ​
            @SuppressLint("InflateParams")
            override fun instantiateItem(container: ViewGroup, position: Int): Any {
                return (LayoutInflater.from(mContext)
                    .inflate(R.layout.vp_item, null) as RecyclerView).apply {
                    layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false)
                    addItemDecoration(DividerItemDecoration(context, RecyclerView.VERTICAL))
                    val mAdapter = AirportFlightAdapter(
                        mData[position].flightInfo,
                        R.layout.view_large_screen_adapter_item
                    )
                    adapter = mAdapter
                    container.addView(this)
                }
            }
    ​
            override fun getItemPosition(`object`: Any): Int {
                return POSITION_NONE
            }
    ​
            override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
                container.removeView(`object` as View)
            }
        }
    ​
        override fun onTouch(v: View?, event: MotionEvent?): Boolean {
            return mDetector.onTouchEvent(event)
        }
    ​
        override fun onShowPress(e: MotionEvent?) {
    ​
        }
    ​
        override fun onSingleTapUp(e: MotionEvent?): Boolean {
            return false
        }
    ​
        override fun onDown(e: MotionEvent?): Boolean {
            return true
        }
    ​
        override fun onFling(
            e1: MotionEvent?,
            e2: MotionEvent?,
            velocityX: Float,
            velocityY: Float
        ): Boolean {
            val e1RawX: Float = e1?.rawX ?: 0f
            val e2RawX: Float = e2?.rawX ?: 0f
            if (abs(e2RawX - e1RawX) > 20) {
                //往右滑
                if (e2RawX - e1RawX > 0) {
    //                Toast.makeText(context, "往右滑--x轴加速度: $velocityX", Toast.LENGTH_SHORT).show()
                    switchPager(mViewPager.currentItem - 1, RIGHT_SCROLL)
                } else if (e2RawX - e1RawX < 0) {
    //                Toast.makeText(context, "往左滑--x轴加速度: $velocityX", Toast.LENGTH_SHORT).show()
                    switchPager(mViewPager.currentItem + 1, LEFT_SCROLL)
                }
                return true
            }
            return true
        }
    ​
        override fun onScroll(
            e1: MotionEvent?,
            e2: MotionEvent?,
            distanceX: Float,
            distanceY: Float
        ): Boolean {
            return false
        }
    ​
        override fun onLongPress(e: MotionEvent?) {
        }
    ​
        private fun dip2px(context: Context?, dipValue: Float?): Int {
            if (context == null || dipValue == null) {
                return 0
            }
            return (context.resources.displayMetrics.density * dipValue).roundToInt()
        }
    ​
        fun getScreenWidth(context: Context?): Int {
            if (context == null) {
                return 0
            }
            return context.resources.displayMetrics.widthPixels
        }
    ​
    }

    mViewPager.setFlingListener来设置ViewPager在onFling时的操作,这个操作就是动画、标题切换和ViewPager显示页切换,同样在封装View本身的onFling时也要触发这一系列操作,封装View本身的手势监听我们用GestureDetector.OnGestureListener来实现,注意我们还实现了View.OnTouchListener的onTouch方法来把触摸事件交给mDetector.onTouchEvent(event)处理,这样就可以监听手势变化。

    滑动触发的操作起点就是switchPager方法,一个参数是ViewPager要切换的新tab的index,另一个是左/右滑动标志,阅读代码可知,它是有一个特定的执行顺序的,比如左滑的顺序就是,mAirport、mTitlePanel、mViewPager向左滑出屏幕,然后调用mViewPager.setCurrentItem()切换tab,同时更改mAirport的值,因为在执行右边划入的时候看到的应该是即将要显示的新的值,所以在此时滑出屏幕后不可见时要默默地切换成新值。然后在右边滑入动画前执行setTranslationX方法让这三个View都移动到屏幕右侧不可见,因为右边滑入动画是从右滑到左,所以在它之前要把起始位置修改到左边,不然只会看到一个左边滑出的reverse效果,可以看成setTranslationX操作是瞬间完成的,看不到从左到右的效果。最后执行右边滑入操作。

    最后要讲一下动画的实现:

    ObjectAnimator.ofFloat方法第一个参数是要执行动画的对象,这里显然是TextView、LinearLayout和ViewPager,第二个参数是要改变的属性,这里是translationX,最后是可变参数,可有多个值,意思是View所在当前坐标轴y轴要移动到的距离,View的位置自然会跟着y轴变化。

    左边滑出时设置的是-(mAirport.width + mAirport.marginStart).toFloat(),因为mAirport初始位置离屏幕左边缘有一个margin,所以向左移动到不可见的话除了要移动一个View的宽度还要移动一个margin的宽度,接下来setTranslationX和属性动画的设置效果一样,mAirport.translationX = getScreenWidth(context).toFloat()就是把mAirport移动到屏幕右边缘外面,最后再调用右边滑入的属性动画把View恢复到原来位置,至此整个效果就完成了。

    三个动画使用AnimatorSet来联动执行,注意mAirport的动画和另外两个View的动画是有时间差值的,左滑的时候mAirport动画先执行,右滑的时候mAirport动画延迟执行。

  • 总结和demo地址

    实现的关键就是translationX属性的属性动画,还有view.setTranslationX(float x)进行平移,他们两个都是相当于移动坐标系y轴,比如说初始位置在距屏幕左侧30像素的位置,然后translationX设置成30(即y轴移动到30的位置上),那此时的y轴就是在30像素的位置上,自然View会向右移动到距离屏幕左侧60像素的位置上,margin也会影响偏移,所以偏移的时候要考虑margin。

    demo地址:目录是JetpackLearning /app /src /main /java /com /mph /jetpackproj /cc_demo /view_pager

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值