温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

Android中怎么实现下拉阻尼效果

发布时间:2021-08-07 15:05:35 来源:亿速云 阅读:133 作者:Leah 栏目:编程语言

Android中怎么实现下拉阻尼效果,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。

原理

这种效果是通过自定义控件的方式来实现的,我自定义了一个控件类型,这个自定义控件(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则注册本监听器的控件将不会对事件做出响应,true则相反   */  @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){          return false;        }        //头部完全隐藏时不再向上滑动        if (vector < 0 && mHeadLayoutParams.topMargin <= mHeadLayoutHeight) {          return false;        }        //头部完全展开时不再向下滑动        if (vector > 0 && mHeadLayoutParams.topMargin >= 0) {          return false;        }        //对增量进行修正,对滑动距离进行减半        int topMargin = mHeadLayoutParams.topMargin + (vector/2);//阻尼值        if(topMargin>0){          // 瞬间拉动的距离超过了头部高度,因为这一瞬间很短,这里采用直接赋值的方式          // 如需平滑过渡,要另开线程,并且监听到ACTION_DOWN时线程可被打断          topMargin = 0;        }        else if(topMargin<mHeadLayoutHeight){          // 瞬间拉动的距离超过了头部高度,因为这一瞬间很短,这里采用直接赋值的方式          // 如需平滑过渡,要另开线程,并且监听ACTION_DOWN时线程可被打断          topMargin = mHeadLayoutHeight;        }        //用户对屏幕的滑动将会改变控件的TopMargin        mHeadLayoutParams.topMargin = topMargin ;        mHeadLayout.setLayoutParams(mHeadLayoutParams);        break;      default:        //TODO 出现其他触碰事件,如MotionEvent.ACTION_UP时,根据阈值判断此时头部应该弹出还是隐藏        mChangeHeadLayoutTopMargin=true;        if(mHeadLayoutParams.topMargin<=mBoundary){          //隐藏          new MoveHeaderTask().execute(true);        }        else{          //展开          new MoveHeaderTask().execute(false);        }        break;    }    return false;  }  /**   * 新线程,隐藏或者展开头部布局,线程可被ACTION_DOWN打断   */  class MoveHeaderTask extends AsyncTask<Boolean, Integer, Integer> {    /**     *     * @param opt true为隐藏动画,false为展开动画     * @return     */    @Override    protected Integer doInBackground(Boolean... opt) {      int topMargin=mHeadLayoutParams.topMargin;      //true为隐藏,false为展开      int speed=(opt[0])?mHeadLayoutHideSpeed:mHeadLayoutUnfoldSpeed;      while(mChangeHeadLayoutTopMargin){        topMargin += speed;        if (topMargin <= mHeadLayoutHeight||topMargin>=0) {          topMargin=(opt[0])?mHeadLayoutHeight:0;          publishProgress(topMargin);          break;        }        publishProgress(topMargin);        sleep(mSleepTime);      }      return null;    }    //调用publishProgress后会执行    @Override    protected void onProgressUpdate(Integer... topMargin) {      mHeadLayoutParams.topMargin=topMargin[0];      mHeadLayout.setLayoutParams(mHeadLayoutParams);    }  }  //调整参数  public void setHeadLayoutHideSpeed(int speed){    this.mHeadLayoutHideSpeed=speed;  }  public void setHeadLayoutUnfoldSpeed(int speed){    this.mHeadLayoutUnfoldSpeed=speed;  }  public void setSleepTime(long time){    this.mSleepTime=time;  }  public void setRatio(double ratio){    this.mRatio=ratio;  }}

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?><android.support.constraint.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:layout_width="match_parent"  android:layout_height="match_parent"  tools:context=".MainActivity">  <com.example.pulldowndumpertest.PullDownDumperLayout    android:tag="记得将这个标签修改为自己的包名"    android:id="@+id/PullDownDumper"    android:layout_width="900px"    android:layout_height="1920px"    app:layout_constraintLeft_toLeftOf="parent"    app:layout_constraintRight_toRightOf="parent"    app:layout_constraintTop_toTopOf="parent"    android:background="@null"    android:orientation="vertical">    <LinearLayout      android:layout_width="match_parent"      android:layout_height="500px"      android:orientation="vertical"      android:background="@color/colorPrimary"      android:clickable="true">      <TextView        android:layout_width="match_parent"        android:layout_height="match_parent"        android:text="隐藏头部"        android:textSize="100px"        android:gravity="center"        android:textColor="#FFFFFF"        android:background="@null"/>    </LinearLayout>    <LinearLayout      android:layout_width="match_parent"      android:layout_height="1700px"      android:background="@color/colorPrimaryDark"      android:clickable="true">      <TextView        android:layout_width="match_parent"        android:layout_height="match_parent"        android:text="可见主体"        android:textSize="100px"        android:gravity="center"        android:textColor="#FFFFFF"        android:background="@null"/>    </LinearLayout>  </com.example.pulldowndumpertest.PullDownDumperLayout></android.support.constraint.ConstraintLayout>

MainActivity.java:

public class MainActivity extends AppCompatActivity {  @Override  protected void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    setContentView(R.layout.activity_main);    //TODO 读者可在这里初始化参数    PullDownDumperLayout pddl=findViewById(R.id.PullDownDumper);  }}

下面是笔者正在使用的自定义控件,比上述的控件多了一个效果:

头部处于隐藏或展开的不同状态时,触发动画效果的分界线可以随状态不同而改变。

还是拿最新版的微信小程序入口来讲,用户在下拉时,小程序界面会占用整个屏幕,如果触发动画的分界线太低,这样导致的结果是用户可能无法通过上滑重新返回联系人列表,但由于微信没有对滑动距离进行减半处理,所以不存在上述问题,可能是出于防止误触的原因,从小程序界面返回联系人列表的方式改用点击底部的一个按钮。而我的控件可以通过改变触发动画效果的分界线来解决这一问题,感兴趣的读者可以研究一下。

public class PullDownDumperLayout extends LinearLayout implements View.OnTouchListener {  /**   * 取布局中的第一个子元素为下拉隐藏头部   */  private View mHeadLayout;  /**   * 隐藏头部布局的高的负值   */  private int mHeadLayoutHeight;  /**   * 隐藏头部的布局参数   */  private MarginLayoutParams mHeadLayoutParams;  /**   * 判断是否为第一次初始化,第一次初始化需要把headView移出界面外   */  private boolean mOnLayoutIsInit=false;  /**   * 从配置获取的滚动判断阈值,为两点间的距离,超过此阈值判断为滚动   *///  private int mScaledTouchSlop;  /**   * 按下时的y轴坐标   *///  private float mDownY;  /**   * 移动时,前一个坐标   */  private float mMoveY;  /**   * 如果为false,会退出头部展开或隐藏动画   */  private boolean mChangeHeadLayoutTopMargin;  /**   * 头部布局的隐藏和展开速度,以及单次执行时间   */  private int mHeadLayoutHideSpeed;  private int mHeadLayoutUnfoldSpeed;  private long mSleepTime;  /**   * 初始化头部布局的偏移值,数值越大,头部可见部分越多,预设值为0,即初始时头部完全不可见   */  private int mTopMarginOffset;  /**   * 触发动画的分界线,头部布局上半部分和整体高度的比例   */  private double mUnfoldRatio;  private double mHideRatio;  /**   * 触发动画的分界线,初始值由mRatio计算得到   * 头部处于隐藏时等于mUnfoldBoundary   * 头部处于展开时等于mHideBoundary   * mBoundary在onTouch的ACTION_DOWN中变化   */  private int mBoundary;  private int mUnfoldBoundary;  private int mHideBoundary;  /**   * 阻尼值,越大越难拖动,呈线性趋势   */  private int mDumper;  public PullDownDumperLayout(Context context, AttributeSet attrs) {    super(context, attrs);//    mScaledTouchSlop= ViewConfiguration.get(context).getScaledTouchSlop();    mHeadLayoutHideSpeed=-30;    mHeadLayoutUnfoldSpeed=30;    mSleepTime=10;    mUnfoldRatio=0.6;    mHideRatio=mUnfoldRatio;    mDumper=2;    mTopMarginOffset=-200;  }  /**   * 布局开始设置每一个控件   * 在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();      mUnfoldBoundary=(int)(mUnfoldRatio*mHeadLayoutHeight);//计算触发展开动画分界线      mHideBoundary=(int)(mHideRatio*mHeadLayoutHeight);//计算触发隐藏动画分界线      mBoundary=mUnfoldBoundary;//触发动画的分界线初始为mUnfoldBoundary      mHeadLayoutHeight-=mTopMarginOffset;//头部隐藏布局可见的部分      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: 注册本监听器的控件将不会对事件做出响应,true则相反   */  @Override  public boolean onTouch(View v, MotionEvent event) {    switch (event.getAction()) {      case MotionEvent.ACTION_DOWN:        //根据此时处于完全展开或完全隐藏决定mBoundary的值,如果两种情况都不满足则不做改变        if(mHeadLayoutParams.topMargin==mHeadLayoutHeight)          mBoundary=mUnfoldBoundary;        else if(mHeadLayoutParams.topMargin==0)          mBoundary=mHideBoundary;//        mDownY=event.getRawY();//获取按下的屏幕y坐标        mMoveY=event.getRawY();        mChangeHeadLayoutTopMargin=false;//false会打断隐藏或展开头部布局的动画        break;      case MotionEvent.ACTION_MOVE:        float currY=event.getRawY();        int vector=(int)(currY-mMoveY);//向量,用于判断手势的上滑和下滑        mMoveY=currY;        //判断是否为滑动        if(Math.abs(vector)==0){          return false;        }        //头部完全隐藏时不再向上滑动        if (vector < 0 && mHeadLayoutParams.topMargin <= mHeadLayoutHeight) {          return false;        }        //头部完全展开时不再向下滑动        else if (vector > 0 && mHeadLayoutParams.topMargin >= 0) {          return false;        }        //对增量进行修正        int topMargin = mHeadLayoutParams.topMargin + (vector/mDumper);        if(topMargin>0){          // 瞬间拉动的距离超过了头部高度,因为这一瞬间很短,这里采用直接赋值的方式          // 如需实现平滑过渡,要另开线程,并且监听到ACTION_DOWN时线程可被打断          topMargin = 0;        }        else if(topMargin<mHeadLayoutHeight){          // 瞬间拉动的距离超过了头部高度,因为这一瞬间很短,这里采用直接赋值的方式          // 如需实现平滑过渡,要另开线程,并且监听ACTION_DOWN时线程可被打断          topMargin = mHeadLayoutHeight;        }        //使参数生效        mHeadLayoutParams.topMargin = topMargin ;        mHeadLayout.setLayoutParams(mHeadLayoutParams);        break;      default:        //出现其他触碰事件,如MotionEvent.ACTION_UP时,根据阈值mBoundary判断此时头部应该弹出还是隐藏        mChangeHeadLayoutTopMargin=true;//允许执行动画        if(mHeadLayoutParams.topMargin<=mBoundary){          //隐藏          new MoveHeaderTask().execute(true);        }        else{          //展开          new MoveHeaderTask().execute(false);        }        break;    }    return false;  }  /**   * 新线程,隐藏或者展开头部布局,线程可被ACTION_DOWN打断   */  private class MoveHeaderTask extends AsyncTask<Boolean, Integer, Integer> {    /**     *     * @param opt true为隐藏动画,false为展开动画     * @return     */    @Override    protected Integer doInBackground(Boolean... opt) {      int topMargin=mHeadLayoutParams.topMargin;      //true为隐藏,false为展开      int speed=(opt[0])?mHeadLayoutHideSpeed:mHeadLayoutUnfoldSpeed;      while(mChangeHeadLayoutTopMargin){        topMargin += speed;        if (topMargin <= mHeadLayoutHeight||topMargin>=0) {          topMargin=(opt[0])?mHeadLayoutHeight:0;          publishProgress(topMargin);          break;        }        publishProgress(topMargin);        sleep(mSleepTime);      }      return null;    }    //调用publishProgress后会执行    @Override    protected void onProgressUpdate(Integer... topMargin) {      mHeadLayoutParams.topMargin=topMargin[0];      mHeadLayout.setLayoutParams(mHeadLayoutParams);    }  }  //调整参数  public void setHeadLayoutHideSpeed(int speed){    this.mHeadLayoutHideSpeed=speed;  }  public void setHeadLayoutUnfoldSpeed(int speed){    this.mHeadLayoutUnfoldSpeed=speed;  }  public void setSleepTime(long time){    this.mSleepTime=time;  }  public void setDumper(int dumper){    this.mDumper=dumper;  }  public void setTopMarginOffset(int offset){    this.mTopMarginOffset=-offset;  }  /**   * 头部处于隐藏状态时,触发展开动画的分界线   * @param ratio 头部布局上部分与下部分的分界线   */  public void setUnfoldRatio(double ratio){    this.mUnfoldRatio=ratio;  }  /**   * 头部处于展开状态时,触发隐藏动画的分界线   * @param ratio 头部布局上部分与下部分的分界线   */  public void setHideRatio(double ratio){    this.mHideRatio=ratio;  }}```

看完上述内容是否对您有帮助呢?如果还想对相关知识有进一步的了解或阅读更多相关文章,请关注亿速云行业资讯频道,感谢您对亿速云的支持。

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI