您好,登录后才能下订单哦!
Android中怎么实现下拉阻尼效果,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。
原理
这种效果是通过自定义控件的方式来实现的,我自定义了一个控件类型,这个自定义控件(PullDownDumperLayout)继承自线性布局(LinearLayout) 。
用户可以下拉弹出的那个视图,例如微信的小程序列表,开发者只是将这个视图移出了父元素之外,所以不可见,我们暂且称之为隐藏头部,只有下拉到一定程度才会弹出,而主体,例如微信的联系人列表,则是可见的,布局见下图。
实现这个效果需要我们做三件工作:
1.隐藏作为头部的控件2.监听用户对屏幕的操作事件3.实现下拉回弹的动画效果
我们这个自定义控件会自动获取内部第一个子元素充当头部,其余的元素则是充当可见的主体(详见代码中的注释)。
基本的布局原理差不多就这样了,但是我们还需要让自定义控件监听用户的手势操作,例如上下滑动等。这里我和灵感来源的那篇博客一样,让自定义控件实现View.OnTouchListener接口,实现内部的onTouch方法可以监听来自屏幕的所有触摸操作。代码中我让头部和第二个子元素(可见的主体)注册了这个监听器,这是为了方便读者理解,读者可根据自己的需求进行修改。
注意,对于不能监听屏幕触摸事件的控件需要添加:
android:clickable="true"
至此,我们已经可以进行布局和监听用户手势了,但是还需要实现一个头部展开和隐藏的动画效果。当用户将隐藏头部下拉或上滑到一定高度时,这个效果就会被触发,这需要依赖上面所述的onTouch方法。动画效果的实现需要另开一个线程进行操作,线程的启动方式我们可以采用继承AsyncTask类来实现。
除此之外,我们可能会多次复用这个控件,所以在自定义控件类的最后还需要一些调整参数的set方法。
这里提个醒,在接下来的代码中,我们的自定义控件因为继承自LinearLayout,里面需要重写onLayout方法,而onLayout方法顾名思义就是布局,这个方法在Activity中的onCreate方法执行之后才会被调用,所以我们可以在Activity的onCreate方法中利用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; }}```
看完上述内容是否对您有帮助呢?如果还想对相关知识有进一步的了解或阅读更多相关文章,请关注亿速云行业资讯频道,感谢您对亿速云的支持。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。