仿微信下拉控件[笔记]

首先看一下效果图:
4593470-92ddcb494b72b229.gif
meter1212.gif
实现方法:

与下拉刷新的实现思路一样(只用把头布局的内容替换了),可以给ListView添加一个头布局,通过手势动态实时的控制头布局的paddingTop值,从而达到显示与隐藏的效果.
我在这里并没有使用ListView,而是使用Linearlayout,但是思路是完全一致的:

  1. 自定义view,继承自LinearLayout,设置布局为vertical
  2. 添加顶部布局,通过设置paddingTop控制显示与隐藏
  3. 添加ListView,触摸监听以及状态处理
  4. 圆点加载动画,手势动画(回弹效果)
步骤:

以下代码使用kotlin
最后有源码!!!

1.自定义view,继承自LinearLayout

设置布局为vertical,代码中设置或者xml中设置

class LoftView : LinearLayout {
  constructor(context: Context) : super(context) {
        LoftView(context, null)
    }
     constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        LoftView(context, null, -1)
    }
     constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
        val obtainStyledAttributes = context.obtainStyledAttributes(attrs, R.styleable.LoftView)
    }
}  TopLayout = LinearLayout(context)
                TopLayout?.setClickable(true)

下图展示了界面显示的状态以及顶部view的paddingTop值的变化

4593470-4e3c1bdc1dfe3aed.png
搜狗截图20180408182100.png

2.添加头部以及滑动控件
  • 当前view是继承自LinearLayout,并且布局方式为vertical,所以先后addView()即可
  • 头部布局包括一个可以自由填充的view和小圆点,自由填充的view通过暴露方法有用户填充,小圆点通过自定义view绘制
  TopLayout = LinearLayout(context)//头部的根布局
  TopLayout?.setClickable(true)//设置可点击,否则可能子view无法响应触摸事件(后面会进一步说明)
  TopLayout?.setBackgroundColor(background_top)//设置背景颜色
  TopLayout?.orientation = LinearLayout.VERTICAL//设置布局为VERTICAL

公开添加头部和滑动view的方法

//添加自定义头部布局
fun createLoftView(context: Context?, resLayout: Int): LinearLayout? {
        headlayout = View.inflate(context, resLayout, null) as LinearLayout?
        headlayout?.visibility = View.INVISIBLE//默认隐藏,滑动动画开始后显示
        return headlayout
    }
/**
     * 可滑动的view
     */
    fun createRefreshView(refreshView: ListView) {
        this.refreshView = refreshView
    }
 TopLayout?.addView(headlayout)
 TopLayout?.addView(dotView, layoutParams)//添加小圆点
 addView(TopLayout)//添加头部
 addView(refreshView)//添加listview
3.测量头部布局的高度,设置paddingTop
  • paddingTop=0时,TopView时正常显示的(state==open)
  • paddingTop= -height,TopView为关闭状态(state==close)
TopLayout?.post({
       subViewHeight = TopLayout?.height as Int
       mPaddingTop = -subViewHeight
       val paddingLeft = TopLayout?.paddingLeft as Int
       val paddingRight = TopLayout?.paddingRight as Int
       mpaddingBottom = TopLayout?.paddingTop as Int
       TopLayout?.setPadding(paddingLeft, mPaddingTop, paddingRight, mpaddingBottom)
                })
//  mPaddingTop = -subViewHeight所以此时的顶部view是隐藏的
4.手势拦截控制
  • 顶部view有两种状态(open,close),手势滑动(向下滑动但view没有完全打开,向下滑动已经已经完全打开,向上滑动)
  • 当view打开时,x轴的偏移量大于y轴的偏移量时,不拦截事件,交由子控件处理
  • listview的第一可见的条目是第0条时,拦截触摸事件,由父控件(当前view)处理,拉出/关闭顶部view,否则不拦截,直到listview滑动到第一个item
  • 当向上滑动, 顶部view为关闭状态时,不拦截事件
  • 按下,抬起不拦截事件
  override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        when ( ev?.action) {
            MotionEvent.ACTION_DOWN ->//安下
            {
                mlastY = ev?.getY()
                mlastX = ev?.getX()
                mTouchEvent = false
            }
            MotionEvent.ACTION_MOVE ->//滑动
            {
                val flX = ev?.getX() - mlastX
                val fl = ev?.getY() - mlastY
                val abs = Math.abs(fl)
                val scaledTouchSlop = ViewConfiguration.get(context).scaledTouchSlop//手指滑动阈值
                if (abs > scaledTouchSlop) {
                    if (refreshView?.firstVisiblePosition == 0) {
                        mTouchEvent = true
                    } else {
                        mTouchEvent = false
                    }

                    if (fl < 0 && stateView == VIewState.CLOSE) {
                        mTouchEvent = false
                    }

                    if (stateView == VIewState.OPEN) {
                        if (Math.abs(flX) > abs) {
                            mTouchEvent = false
                        }
                    }
                }
            }
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> mTouchEvent = false//抬起
        }
        return mTouchEvent
    }
5.手势控制
  • 当在关闭状态向下滑动并且没有完全打开时
//scrollYValue是y轴偏移量为向下为正,增大
  mPaddingTop = (decay_ratio * scrollYValue - subViewHeight).toInt()//decay_ratio * scrollYValue = subViewHeight时,view完全显示
  TopLayout?.setPadding(paddingLeft, mPaddingTop, paddingRight, mpaddingBottom)
  • 完全显示依然向下,增加阻尼力度
 mPaddingTop = (0.5 * decay_ratio * scrollYValue + 0.5 * (-subViewHeight)).toInt()
 TopLayout?.setPadding(paddingLeft, mPaddingTop, paddingRight, mPaddingTop)
  • 按下之前是打开状态,向下滑动时
   mPaddingTop = (0.5 * decay_ratio * scrollYValue).toInt()
   TopLayout?.setPadding(paddingLeft, mPaddingTop, paddingRight, mPaddingTop)//PaddingTop,paddingBottom同时设置为mPaddingTop,使view在滑动时居中
  • 打开状态向上滑动
  mPaddingTop = (decay_ratio * scrollYValue).toInt()
  TopLayout?.setPadding(paddingLeft, mPaddingTop, paddingRight, mpaddingBottom)
6.抬起以及动画控制
  • 向下滑动超过高度的2/3时手指抬起:
 moveAnimation(-mPaddingTop, mPaddingTop)//滚动打开view
 stateView = VIewState.OPEN//状态设置为OPEN
 dotHideAnim()//小圆点隐藏动画
  • 向下滑动没有超过高度的2/3时手指抬起:
moveAnimation(-mPaddingTop, subViewHeight)//滚动关闭view
stateView = VIewState.CLOSE//状态设置为CLOSE
headlayout?.visibility = View.INVISIBLE//头部隐藏
dotView?.alpha = 1.0f//隐藏小圆点
  • 完全打开后抬起:
 moveAnimation(-mPaddingTop, mPaddingTop)//滚动到view原始大小位置
 stateView = VIewState.OPEN//状态设置为OPEN
 dotHideAnim()//隐藏小圆点
  • 向上滑动后抬起:
moveAnimation(-mPaddingTop, subViewHeight)//滚动关闭view
headlayout?.visibility = View.INVISIBLE//头部隐藏
stateView = VIewState.CLOSE//状态设置为CLOSE
dotView?.alpha = 1.0f//隐藏小圆点
7.滚动过渡动画(回弹效果)

var mScroller: Scroller = Scroller(context, DecelerateInterpolator())//滑动器

 /**
  * view滚动回弹动画
  */
 fun moveAnimation(startY: Int, y: Int) {
//前两个参数是起始位置,后两个参数分别表示x轴,y轴的偏移量,最后的参数是滚动时间
     mScroller?.startScroll(0, startY, 0, y, 400);
     invalidate()//刷新view会回调到computeScroll()方法,可以拿到一组偏移量的变化值
 }
 override fun computeScroll() {
     if (mScroller.computeScrollOffset()) {
         val currY = mScroller?.getCurrY()//实时获取y轴的偏移值
         TopLayout?.setPadding(paddingLeft, -currY, paddingRight, mpaddingBottom)
     }
     invalidate()//刷新view
 }

小圆点动画以及顶部view向下平移动画分别使用补间动画和属性动画,比较简单不用解释了

8.小圆点的绘制

自定义view,重点有注释,(小圆点的透明度的控制可以忽略)

 override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        val centerX = (width / 2).toFloat()
        val centerY = (height / 2).toFloat()
//percent是view显示部分的百分比,完全打开时为1,关闭时为0
        val fl = 255 * percent * 1.5f+30//控制透明度
        mPaint.alpha = if(fl>255) 255 else fl.toInt()
        if (percent <= 0.3f) {//小于1/3画一个圆圈
            val radius = percent * 3.33f * maxRadius
            canvas.drawCircle(centerX, centerY, radius, mPaint)
        } else {//大于2/3画三个个圆
            val afterPercent = (percent - 0.3f) / 0.7f
            if (afterPercent<=1) {
                val radius = maxRadius - maxRadius / 2f * afterPercent
                Log.e("afterPercent--->", afterPercent.toString())
                canvas.drawCircle(centerX, centerY, radius, mPaint)
                canvas.drawCircle(centerX - afterPercent * maxDist, centerY, maxRadius / 2, mPaint)
                canvas.drawCircle(centerX + afterPercent * maxDist, centerY, maxRadius / 2, mPaint)
            }else if (afterPercent>1){//完全显示依然在向下滑动时,afterPercent会大于1
                var d = afterPercent - 1.0
                d= if (d>1) 1.0 else d
                val fl =(1-d*2) * 255
                mPaint.alpha = if(fl<60) 0 else fl.toInt()
                canvas.drawCircle(centerX, centerY, maxRadius/2, mPaint)
                canvas.drawCircle(centerX - maxDist, centerY, maxRadius / 2, mPaint)
                canvas.drawCircle(centerX +  maxDist, centerY, maxRadius / 2, mPaint)
            }
        }
    }
源码
class LoftView : LinearLayout {
    var headlayout: LinearLayout? = null//顶部view的子控件,由用户添加
    var mScroller: Scroller = Scroller(context, DecelerateInterpolator())//滑动器
    var mTouchEvent: Boolean = false//是否拦截点击事件
    var scrollYValue = 0f//手指Y轴滑动的距离
    var PLACE = 0//顶部view的显示位置(顶部或底部,)
    var subViewHeight = 0//顶部view的height
    var refreshView: ListView? = null
    var decay_ratio = 0.5   //阻尼系数
    var mpaddingBottom = 0//顶部view的paddingBottom
    var mPaddingTop = 0//顶部view的PaddingTop
    var mlastY = 0f//手指安下的y轴坐标值
    var mlastX = 0f//手指安下的y轴坐标值
    var stateView = VIewState.CLOSE//顶部view的显示状态,默认是关闭状态
    var stateMove = TouchState_Move.NORMAL//手势滑动状态
    var dotView: DotView? = null//小圆点
    var TopLayout: LinearLayout? = null//顶部view的父控件
    var background_top: Int = 0xFFe7e7e7.toInt() //默认颜色

    constructor(context: Context) : super(context) {
        LoftView(context, null)
    }

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
        LoftView(context, null, -1)
    }

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
        val obtainStyledAttributes = context.obtainStyledAttributes(attrs, R.styleable.LoftView)
        PLACE = obtainStyledAttributes.getInt(R.styleable.LoftView_place, 0)
        background_top = obtainStyledAttributes.getColor(R.styleable.LoftView_background_top, 0xFFe7e7e7.toInt())

    }

    /**
     * 可滑动的view
     */
    fun createRefreshView(refreshView: ListView) {
        this.refreshView = refreshView
    }

    /**
     * 子view
     */
    fun createLoftView(context: Context?, resLayout: Int): LinearLayout? {
        headlayout = View.inflate(context, resLayout, null) as LinearLayout?
        headlayout?.visibility = View.INVISIBLE
        return headlayout
    }

    /**
     * 构建顶部view布局
     */
    fun buildView() {
        when (refreshView) {
            null -> refreshView = ListView(context)
        }
        when (headlayout) {
            null -> throw LoftException("未初始化下拉view")
            else -> {
                dotView = DotView(context!!)
                val layoutParams = LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, 40)
                TopLayout = LinearLayout(context)
                TopLayout?.setClickable(true)
                TopLayout?.setBackgroundColor(background_top)
                TopLayout?.orientation = LinearLayout.VERTICAL
                TopLayout?.post({
                    subViewHeight = TopLayout?.height as Int
                    mPaddingTop = -subViewHeight
                    val paddingLeft = TopLayout?.paddingLeft as Int
                    val paddingRight = TopLayout?.paddingRight as Int
                    mpaddingBottom = TopLayout?.paddingTop as Int
                    TopLayout?.setPadding(paddingLeft, mPaddingTop, paddingRight, mpaddingBottom)
                })
                //暂不支持底部显示
                when (PLACE) {
                    0, 1 -> {
                        TopLayout?.addView(headlayout)
                        TopLayout?.addView(dotView, layoutParams)
                        addView(TopLayout)
                        addView(refreshView)
                    }
//可以忽略
//                    1 -> {
//                        this.addView(refreshView)
//                        TopLayout?.addView(expendPoint, layoutParams)
//                        TopLayout?.addView(headlayout)
//                        this.addView(TopLayout)
//                    }
                }
            }
        }
    }

    /**
     * 设置顶部背景颜色
     */
    fun setBackgroundTop(color: Int) {
        this.background_top = color
    }

    /**
     * 处理触摸事件
     */
    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        when ( ev?.action) {
            MotionEvent.ACTION_DOWN ->//安下
            {
                mlastY = ev?.getY()
                mlastX = ev?.getX()
                mTouchEvent = false
            }
            MotionEvent.ACTION_MOVE ->//滑动
            {
                val flX = ev?.getX() - mlastX
                val fl = ev?.getY() - mlastY
                val abs = Math.abs(fl)
                val scaledTouchSlop = ViewConfiguration.get(context).scaledTouchSlop//手指滑动阈值
                if (abs > scaledTouchSlop) {

                    if (refreshView?.firstVisiblePosition == 0) {
                        mTouchEvent = true
                    } else {
                        mTouchEvent = false
                    }

                    if (fl < 0 && stateView == VIewState.CLOSE) {
                        mTouchEvent = false
                    }

                    if (stateView == VIewState.OPEN) {
                        if (Math.abs(flX) > abs) {
                            mTouchEvent = false
                        }
                    }
                }
            }
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> mTouchEvent = false//抬起
        }
        return mTouchEvent
    }

    override fun onTouchEvent(ev: MotionEvent?): Boolean {
        val action = ev?.action;
        when (action) {
            MotionEvent.ACTION_DOWN -> mTouchEvent = true//安下
            MotionEvent.ACTION_MOVE ->//滑动
            {
                scrollYValue = (ev?.getY() - mlastY)
                val abs = Math.abs(scrollYValue)
                val scaledTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
                if (abs > scaledTouchSlop) {
                    mTouchEvent = true
                    if (scrollYValue > 0) {
                        if (refreshView?.firstVisiblePosition == 0) {
                            if (stateView == VIewState.CLOSE) {
                                if (mPaddingTop < 0) {//向下滑动但是头部空间没完全显示
                                    mPaddingTop = (decay_ratio * scrollYValue - subViewHeight).toInt()
                                    TopLayout?.setPadding(paddingLeft, mPaddingTop, paddingRight, mpaddingBottom)
                                    stateMove = TouchState_Move.DOWN_NO_OVER
                                    dotView?.setPercent(1 - (mPaddingTop.toFloat() / (-subViewHeight)))
                                    if (mPaddingTop > -subViewHeight / 2) {
                                        showTodown(headlayout!!, 400)
                                    }
                                } else if (mPaddingTop >= 0) {//头部空间没哇完全显示依然向下滑动
                                    mPaddingTop = (0.5 * decay_ratio * scrollYValue + 0.5 * (-subViewHeight)).toInt()
                                    TopLayout?.setPadding(paddingLeft, mPaddingTop, paddingRight, mPaddingTop)
                                    stateMove = TouchState_Move.DOWN_OVER
                                    dotView?.setPercent(1 - (mPaddingTop.toFloat() / (-subViewHeight)))
                                }
                            } else {
                                mPaddingTop = (0.5 * decay_ratio * scrollYValue).toInt()
                                TopLayout?.setPadding(paddingLeft, mPaddingTop, paddingRight, mPaddingTop)
                                stateMove = TouchState_Move.DOWN_OVER
                            }
                        }
                    } else {
                        if (refreshView?.firstVisiblePosition == 0) {
                            if (stateView == VIewState.CLOSE) {
                                mPaddingTop = -subViewHeight
                            } else {
                                mPaddingTop = (decay_ratio * scrollYValue).toInt()
                                if (mPaddingTop <= -subViewHeight) {
                                    TopLayout?.setPadding(paddingLeft, mPaddingTop, paddingRight, mpaddingBottom)
                                    mPaddingTop = -subViewHeight
                                    stateView == VIewState.CLOSE
                                } else {
                                    TopLayout?.setPadding(paddingLeft, mPaddingTop, paddingRight, mpaddingBottom)
                                    stateMove = TouchState_Move.UP
                                }
                            }
                        }
                    }
                }
            }
            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL ->//抬起
            {
                if (mPaddingTop > -subViewHeight / 3 && mPaddingTop < 0 && stateMove == TouchState_Move.DOWN_NO_OVER) {
                    moveAnimation(-mPaddingTop, mPaddingTop)
                    stateView = VIewState.OPEN
                    dotHideAnim()
                }
                if (mPaddingTop <= -subViewHeight / 3 && mPaddingTop < 0 && stateMove == TouchState_Move.DOWN_NO_OVER) {
                    moveAnimation(-mPaddingTop, subViewHeight)
                    stateView = VIewState.CLOSE
                    headlayout?.visibility = View.INVISIBLE
                    dotView?.alpha = 1.0f
                }
                if (stateMove == TouchState_Move.DOWN_OVER) {
                    moveAnimation(-mPaddingTop, mPaddingTop)
                    stateView = VIewState.OPEN
                    dotHideAnim()
                }
                if (stateMove == TouchState_Move.UP) {
                    moveAnimation(-mPaddingTop, subViewHeight)
                    headlayout?.visibility = View.INVISIBLE
                    stateView = VIewState.CLOSE
                    dotView?.alpha = 1.0f
                }
                mTouchEvent = false
                scrollYValue = 0f
                mlastY = 0f
            }
        }

        return mTouchEvent
    }

    /**
     * view滚动回弹动画
     */
    fun moveAnimation(startY: Int, y: Int) {
        mScroller?.startScroll(0, startY, 0, y, 400);
        invalidate()
    }

    override fun computeScroll() {
        if (mScroller.computeScrollOffset()) {
            val currY = mScroller?.getCurrY()
            TopLayout?.setPadding(paddingLeft, -currY, paddingRight, mpaddingBottom)
        }
        invalidate()//刷新view
    }

    /**
     * 触摸状态
     * DOWN_NO_OVER 向下滑动但是没有超出view的height值
     * DOWN_OVER 向下滑动并且超出了height值
     * UP 向上滑动
     * NORMAL 无状态
     */
    enum class TouchState_Move {
        DOWN_NO_OVER, DOWN_OVER, UP, NORMAL
    }

    /**
     * 顶部view的显示状态
     * CLOSE 顶部为关闭
     * OPEN 顶部为打开状态
     */
    enum class VIewState {
        CLOSE, OPEN
    }
    /**
     * 顶部view向下平移动画
     * @param view
     * @param time 动画时间
     */
    fun showTodown(view: View, time: Long) {
        if (view.visibility != View.VISIBLE) {
            val animator1 = ObjectAnimator.ofFloat(view, "translationY", -50f, 0f)
            animator1.setInterpolator(AccelerateDecelerateInterpolator())
            animator1.setDuration(time).start()
            animator1.addListener(object : Animator.AnimatorListener {
                override fun onAnimationStart(animator: Animator) {}

                override fun onAnimationEnd(animator: Animator) {
                    view.visibility = View.VISIBLE
                }

                override fun onAnimationCancel(animator: Animator) {}

                override fun onAnimationRepeat(animator: Animator) {

                }
            })
        }
    }

    /**
     * 小圆点的隐藏动画
     */
    fun dotHideAnim() {
        val alpha = dotView?.animate()?.alpha(0f)
        alpha?.duration = 400
        alpha?.start()
    }
}

自定义属性

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="LoftView">
        <attr name="place" format="enum">
            <enum name="top" value="0"/>
            <enum name="bottom" value="1"/>
        </attr>
        <attr name="background_top" format="color"/>
    </declare-styleable>
</resources>

小圆点

class DotView : View {

    internal var percent: Float = 0.toFloat()
    internal var maxRadius = 10f
    internal var maxDist = 30f
    internal var mPaint: Paint

    init {
        mPaint = Paint()
        mPaint.isAntiAlias = true
        mPaint.color = Color.GRAY
    }

    public constructor(context: Context) : super(context) {
        DotView(context, null)
    }

    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
    }

    fun setPercent(percent: Float) {
        this.percent = percent
        invalidate()
    }

    override fun onDraw(canvas: Canvas) {
        super.onDraw(canvas)
        val centerX = (width / 2).toFloat()
        val centerY = (height / 2).toFloat()
        val fl = 255 * percent * 1.5f + 30
        mPaint.alpha = if (fl > 255) 255 else fl.toInt()
        if (percent <= 0.3f) {
            val radius = percent * 3.33f * maxRadius
            canvas.drawCircle(centerX, centerY, radius, mPaint)
        } else {//画三个个圆
            val afterPercent = (percent - 0.3f) / 0.7f
            if (afterPercent <= 1) {
                val radius = maxRadius - maxRadius / 2f * afterPercent
                Log.e("afterPercent--->", afterPercent.toString())
                canvas.drawCircle(centerX, centerY, radius, mPaint)
                canvas.drawCircle(centerX - afterPercent * maxDist, centerY, maxRadius / 2, mPaint)
                canvas.drawCircle(centerX + afterPercent * maxDist, centerY, maxRadius / 2, mPaint)
            } else if (afterPercent > 1) {
                var d = afterPercent - 1.0
                d = if (d > 1) 1.0 else d
                val fl = (1 - d * 2) * 255
                mPaint.alpha = if (fl < 60) 0 else fl.toInt()
                canvas.drawCircle(centerX, centerY, maxRadius / 2, mPaint)
                canvas.drawCircle(centerX - maxDist, centerY, maxRadius / 2, mPaint)
                canvas.drawCircle(centerX + maxDist, centerY, maxRadius / 2, mPaint)
            }
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

truemi.73

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值