重庆分公司,新征程启航
为企业提供网站建设、域名注册、服务器等服务
Android中怎么实现下拉阻尼效果,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。
创新互联专业提供郑州服务器托管服务,为用户提供五星数据中心、电信、双线接入解决方案,用户可自行在线购买郑州服务器托管服务,并享受7*24小时金牌售后服务。
原理
这种效果是通过自定义控件的方式来实现的,我自定义了一个控件类型,这个自定义控件(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
activity_main.xml:
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
看完上述内容是否对您有帮助呢?如果还想对相关知识有进一步的了解或阅读更多相关文章,请关注创新互联行业资讯频道,感谢您对创新互联的支持。