Android安卓下拉阻尼效果实现原理及简单实例

本文通过代码讲解Android下拉阻尼效果的实现原理,介绍了一个自定义控件PullDownDumperLayout,该控件继承自LinearLayout,能够监听用户手势并实现下拉回弹动画。详细阐述了布局、监听和动画实现的步骤,并提供了代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

转载请注明出处:codog_main的博客

前言

  本文将通过代码讲解下拉阻尼效果的实现原理。
  实现灵感来源于这篇博客,但是这篇博客的代码并不能让我满意,或者说是糟糕的,不过还是非常感谢作者带给我的启发。
  现在大部分资讯类安卓APP都有一个下拉刷新的功能,又如微信联系人列表顶部的小程序入口,也使用了这种下拉阻尼的效果。
  我的代码主要是解释其实现原理,为方便读者理解,所以代码逻辑非常简单,但如果想要实现例如下拉刷新转动的进度圈,还需要修改代码中的MoveHeaderTask类中的onProgressUpdate方法;如果要实现滑动列表顶部加入这种下拉阻尼效果,则需要修改代码中的onTouch方法,通过判断是否到达列表顶部来决定是否触发下拉阻尼效果的逻辑代码。
  最新的微信版本还实现了一个具有惯性的滑动列表(不清楚这样表述是否正确),滑动的速度大小和小程序入口的下拉阻尼效果会形成互动,但这已不是本文讨论的重点,这需要感兴趣的读者自行对我的代码进行迭代。
  运行效果如下:

  如图,拉动"可见主体"到达一定高度,"隐藏头部"就会弹出,反之,向上滑动到一定高度,"隐藏头部"则会收回,如果未到达指定高度,则恢复原状。
  实际运行效果其实很流畅,也不会出现上图中,头部无法完全隐藏的情况,只是AS自带的录屏工具比较差劲。我不建议把这个自定义控件用在对话框类型的activity上,因为前一个activity处于可见状态,可能会占用大量算力,导致动画效果不流畅,亲测。

原理

  这种效果是通过自定义控件的方式来实现的,我自定义了一个控件类型,这个自定义控件(PullDownDumperLayout)继承自线性布局(LinearLayout)。
  用户可以下拉弹出的那个视图,例如微信的小程序列表,开发者只是将这个视图移出了父元素之外,所以不可见,我们暂且称之为隐藏头部,只有下拉到一定程度才会弹出,而主体,例如微信的联系人列表,则是可见的,布局见下图。

实现这个效果需要我们做三件工作:

  1. 隐藏作为头部的控件
  2. 监听用户对屏幕的操作事件
  3. 实现下拉回弹的动画效果

  我们这个自定义控件会自动获取内部第一个子元素充当头部,其余的元素则是充当可见的主体(详见代码中的注释)。
  基本的布局原理差不多就这样了,但是我们还需要让自定义控件监听用户的手势操作,例如上下滑动等。这里我和灵感来源的那篇博客一样,让自定义控件实现View.OnTouchListener接口,实现内部的onTouch方法可以监听来自屏幕的所有触摸操作。代码中我让头部和第二个子元素(可见的主体)注册了这个监听器,这是为了方便读者理解,读者可根据自己的需求进行修改。
  注意,对于不能监听屏幕触摸事件的控件需要添加:
    android:clickable="true"
  至此,我们已经可以进行布局和监听用户手势了,但是还需要实现一个头部展开和隐藏的动画效果。当用户将隐藏头部下拉或上滑到一定高度时,这个效果就会被触发,这需要依赖上面所述的onTouch方法。动画效果的实现需要另开一个线程进行操作,线程的启动方式我们可以采用继承AsyncTask类来实现。
  除此之外,我们可能会多次复用这个控件,所以在自定义控件类的最后还需要一些调整参数的set方法。
  这里提个醒,在接下来的代码中,我们的自定义控件因为继承自LinearLayout,里面需要重写onLayout方法,而onLayout方法顾名思义就是布局,这个方法在Activity中的onCreate方法执行之后才会被调用,所以我们可以在ActivityonCreate方法中利用findViewById获取实例,调用上面提到的set方法进行参数的初始化。
  LinearLayout中不止onLayout一个方法,详细解析请读者移步其他关于XML标签加载过程的文章,这里不做赘述。

代码

PullDownDumperLayout .java:

public class PullDownDumperLayout extends LinearLayout implements View.OnTouchListener {
   

    /**
     * 取布局中的第一个子元素为下拉隐藏头部
     */
    private View mHeadLayout;

    /**
     * 隐藏头部布局的高的负值
     */
    private int mHeadLayoutHeight;

    /**
     * 隐藏头部的布局参数
     */
    private MarginLayoutParams mHeadLayoutParams;

    /**
     * 判断是否为第一次初始化,第一次初始化需要把headView移出界面外
     */
    private boolean mOnLayoutIsInit=false;

    /**
     * 移动时,前一个坐标
     */
    private float mMoveY;

    /**
     * 如果为false,会退出头部展开或隐藏动画
     */
    private boolean mChangeHeadLayoutTopMargin;

    /**
     * 触发动画的分界线,由mRatio计算得到
     */
    private int mBoundary;

    /**
     * 头部布局的隐藏和展开速度,以及单次执行时间
     */
    private int mHeadLayoutHideSpeed;
    private int mHeadLayoutUnfoldSpeed;
    private long mSleepTime;

    /**
     * 触发动画的分界线,头部布局上半部分和整体高度的比例
     */
    private double mRatio;

    public PullDownDumperLayout(Context context, AttributeSet attrs) {
   
        super(context, attrs);
        //初始化参数,根据自己的需求调整
        mHeadLayoutHideSpeed=-20;
        mHeadLayoutUnfoldSpeed=20;
        mSleepTime=10;
        mRatio=0.5;
    }

    /**
     * 布局开始设置每一个控件
     * 在activity的onCreate执行之后才会执行
     * 因此可以在onCreate中调用set方法设置参数
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
   
        super.onLayout(changed, l, t, r, b);
        if(!mOnLayoutIsInit && changed) {
   
            //将第一个子元素作为头部移出界面外
            mHeadLayout = this.getChildAt(0);
            mHeadLayoutHeight=-mHeadLayout.getHeight();
            mBoundary=(int)(mRatio*mHeadLayoutHeight);//计算触发动画分界线
            mHeadLayoutParams=(MarginLayoutParams) mHeadLayout.getLayoutParams();
            mHeadLayoutParams.topMargin=mHeadLayoutHeight;
            mHeadLayout.setLayoutParams(mHeadLayoutParams);
            //TODO 设置手势监听器,不能触碰的控件需要添加android:clickable="true"
            getChildAt(1).setOnTouchListener(this);
            mHeadLayout.setOnTouchListener(this);
            //标记已被初始化
            mOnLayoutIsInit=true;
        }
    }

    /**
     * 屏幕触摸操作监听器
     * @return 返回false表示在执行onTouch后会继续执行onTouchEvent
     */
    @Override
    public boolean onTouch(View v, MotionEvent event) {
   
        switch (event.getAction()) {
   
            case MotionEvent.ACTION_DOWN:
                mMoveY=event.getRawY();//捕获按下时的坐标,初始化mMoveY
                mChangeHeadLayoutTopMargin=false;
                break;
            case MotionEvent.ACTION_MOVE:
                float currY=event.getRawY();
                int vector=(int)(currY-mMoveY);//向量,用于判断手势的上滑和下滑
                mMoveY=currY;
                //判断是否为滑动
                if(Math.abs(vector)==0)<
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值