Android自制滑动删除Activity组件
来源:互联网 发布:淘宝企业店铺 天猫 编辑:程序博客网 时间:2024/06/05 03:21
今天在做项目的时候,看到iOS端的实现效果是二级页面可以手指在界面上向右滑动关闭页面,而不再是右上角的返回按钮(说实在的,不能单手操作是Android的痛).
于是思考Android上如何实现此功能.
为避免重复造轮子,首先肯定是google,百度了.
很好,网上的各种方案都是在Activity中监听手势,然后实现伪滑动关闭页面.这个方案不是我想要的.
还有一种方案,编写一个可监听滑动的布局,然后在每个想要滑动的activity中作为根布局.这种方案,作者只是提出了思路,并没有完整的思路,但却给了我启发.
思路如下:
1. 首先,Activity是无法控制移动的,但是他其中的View却可以,我们只需要控制View整体跟随手指进行移动即可.
2. 基于第一条,我们需要获取到Activity的根View, 然后监听View的touchEvent.根据手指移动距离实现View中的内容平滑滚动.
3. 基于第2条,如果界面中没有ListView,ScrollView等能够截获事件焦点的控件的话,一切正常. 一旦出现这些控件,我们的滑动将被提前拦截,导致无法实现滑动关闭页面.
4. 于是,我们必须要对事件进行提前拦截,即在View的interceptTouchEvent中作拦截.但显然,对于我们直接获取到的View是无法重写它的intercept逻辑的.因此我们必须重新实现一个能够拦截事件的Layout.
5. 假如此Layout可以正常工作了,能够实现监听手指滑动,也可以提前抢占焦点了,但如何让这个Layout嵌入到我们的Activity中去呢?
6. 一种方案显然可以在xml中将此Layout作为根布局来使用.但总觉得与现有系统耦合的太紧,不具有热插拔能力(就是通过添加/注释一行代码即可实现的能力).
7. 我的目标是要能够通过一行代码解决此问题!
8. 如何既不在xml中加入定制的Layou又能用一行代码解决问题呢?
9. 我起初的思路是,改造Activity,实现一个拥有此功能的抽象类.然后 希望滑动关闭的Activity都来继承此类就可以了.
10. 很好,通过上边代码,我只需要更改现有设计的继承类即可实现目标.
11. 但是很快问题出现了,系统中有的界面本身继承的是FragmentActivity,有的继承自AppCompatActivity,等等.直接改变继承关系导致原有设计功能上的缺失和改变!
12. 不用继承的话,我又该如何作呢?灵光一现,是否可以通过类化:FlipHelper.inject(activity)来达到同样的目的呢,说干就干. 通过在一个静态类FlipHelper中注入界面activity.然后获取到Activity的根View…最后实现目标.
13. 整个过程的实现,变的非常独立,不依赖于任何其他功能.甚至最终我将所有的实现代码写在了一个类中(FlipHelper).也只有区区200行(含注释).
完整的演示代码:https://github.com/andrewlu1/CustomView/
现贴出来,共同分析实现过程:
package cn.andrewlu.app.customview;import android.animation.Animator;import android.animation.ObjectAnimator;import android.animation.ValueAnimator;import android.app.Activity;import android.content.Context;import android.graphics.Canvas;import android.graphics.Color;import android.graphics.LinearGradient;import android.graphics.Paint;import android.graphics.PointF;import android.graphics.Shader;import android.util.Log;import android.view.Gravity;import android.view.MotionEvent;import android.view.View;import android.view.ViewGroup;import android.view.Window;import android.widget.FrameLayout;/** * Created by andrewlu on 16-1-15. * 通过向activity的结构中注入一层能够拦截事件的View布局,从而可以让内容页跟着手指移动. */public final class FlipHelper { public static void inject(Activity activity) { ViewGroup root = (ViewGroup) activity.getWindow().findViewById(Window.ID_ANDROID_CONTENT); if (root == null || root.getChildAt(0) == null) { throw new RuntimeException("inject method must called after setContentView"); } //向其中注入一层布局. HDraggableLayout container = new HDraggableLayout(activity); // container.setBackgroundColor(Color.argb(100, 0, 0, 0)); container.setContentDescription("the injected view."); View child = root.getChildAt(0); child.setBackgroundColor(Color.WHITE); ViewGroup.LayoutParams p = child.getLayoutParams(); //new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); root.addView(container, p); root.removeView(child); container.addView(child); }}/** * Created by andrewlu on 16-1-15. * 可以在水平方向拖动的水平滚动布局.表现效果为:当手指向右滑动时,内容会根据手指位置向右一起滑动. * 以后可能会扩展向左滑动功能.现在不作讨论. * 只能通过new创建出来.因此只实现一个构造函数. */class HDraggableLayout extends FrameLayout { private SimpleGestureListener onGestureListener = new SimpleGestureListener(); public HDraggableLayout(Context context) { super(context); View shadowView = new ShadowView(context); LayoutParams p = new LayoutParams(20, ViewGroup.LayoutParams.MATCH_PARENT); p.gravity = Gravity.LEFT | Gravity.TOP; p.leftMargin = -20; addView(shadowView, p); } //实现定向/定量的滚动事件拦截. private PointF mTouchPoint = new PointF(); private PointF mTouchPointDist = new PointF(); private final static int MIN_SLOP = 24; private long eventTime = 0; @Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: { mTouchPoint.x = ev.getRawX(); mTouchPoint.y = ev.getRawY(); eventTime = ev.getEventTime(); onGestureListener.onDown(ev); break; } case MotionEvent.ACTION_MOVE: { mTouchPointDist.x = ev.getRawX() - mTouchPoint.x; mTouchPointDist.y = ev.getRawY() - mTouchPoint.y; //只负责起始时的拦截.拦截后,事件就完全交给touch来处理了. if (Math.abs(mTouchPointDist.y) > Math.abs(mTouchPointDist.x)) break; if (mTouchPointDist.x < MIN_SLOP) break; Log.i("onInterceptTouchEvent", String.format("dx:%f,dy:%f", mTouchPointDist.x, mTouchPointDist.y)); return true; } } return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_MOVE: { mTouchPointDist.x = ev.getRawX() - mTouchPoint.x; mTouchPointDist.y = ev.getRawY() - mTouchPoint.y; //负责滑动距离的计算. if (getScrollX() - mTouchPointDist.x > 0) { mTouchPointDist.x = getScrollX(); } mTouchPoint.x = ev.getRawX(); mTouchPoint.y = ev.getRawY(); onGestureListener.onScroll(mTouchPointDist.x, mTouchPointDist.y); onGestureListener.onFling(mTouchPointDist.x * 1000 / (ev.getEventTime() - eventTime), mTouchPointDist.y * 1000 / (ev.getEventTime() - eventTime)); eventTime = ev.getEventTime(); break; } case MotionEvent.ACTION_UP: { onGestureListener.onUp(); break; } } return super.onTouchEvent(ev); } private ValueAnimator backAnim, finishAnim; //回滚 private void scrollBack() { backAnim = ObjectAnimator.ofInt(this, "scrollX", getScrollX(), 0).setDuration(200); backAnim.start(); } //滚动到结束. private void scrollFinish() { finishAnim = ObjectAnimator.ofInt(this, "scrollX", getScrollX(), -getWidth()).setDuration(200); finishAnim.addListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animation) { } @Override public void onAnimationEnd(Animator animation) { Context c = getContext(); if (c instanceof Activity) { ((Activity) c).onBackPressed(); } } @Override public void onAnimationCancel(Animator animation) { } @Override public void onAnimationRepeat(Animator animation) { } }); finishAnim.start(); } private class SimpleGestureListener { public boolean onDown(MotionEvent ev) { return false; } public boolean onScroll(float distanceX, float distanceY) { scrollBy(-(int) distanceX, 0); return true; } public boolean onFling(float velocityX, float velocityY) { Log.i("onFling", String.format("vX:%f,vY:%f", velocityX, velocityY)); return false; } public void onUp() { Log.i("onUp", "====================="); if (Math.abs(getScrollX()) < getWidth() / 2) { scrollBack(); } else { scrollFinish(); } } }}//实现一个带阴影的控件.阴影呈现在左边.class ShadowView extends View { private Paint shadowPaint = new Paint(); public ShadowView(Context context) { super(context); } @Override protected void onDraw(Canvas canvas) { Shader mShader = new LinearGradient(getWidth(), 0, 0, 0, new int[]{Color.GRAY, Color.TRANSPARENT}, null, Shader.TileMode.CLAMP); shadowPaint.setShader(mShader); canvas.drawRect(0, 0, getWidth(), getHeight(), shadowPaint); }}
下面帖出完整的代码解释:
1. 核心函数:Fliphelper.inject(Activity)实现了向Activity的View根层级注入一层自定义的Layout. 这样可以省去在Xml中修改根布局的问题.
具体的做法,就是通过
ViewGroup root = (ViewGroup) activity.getWindow().findViewById(Window.ID_ANDROID_CONTENT);
可以得到包含Activity中的界面的根布局.注意,这里是不是xml文件中的那个根布局,而是嵌套在xml外层的一个根布局,通常是一个FrameLayout.
而root.getChildAt(0)就能够得到activity的实际布局内容了.也就是xml所实例化出来的View.
接下来我们需要构造我们自定义的HDraggableLayout对象,并将它添加到root中去.同时,将root.getChildAt(0)从原有布局即root中移除,并添加到我们自定义的布局对象中去.
这样我们的View层级中多出了一个可以拦截事件的布局(至少我们期望的是它能够在适当的时候拦截我们的触摸事件,而不是拦截所有,否则界面中的所有控件都无法响应滑动事件了)
接着我们来看这一层能够拦截事件的Layout如何实现:
很简单,我们在HDraggableLayout的 onInterceptTouchEvent()函数中,拦截MOVE事件,当满足一定的条件的时候,就返回true,表示我们要拦截这个事件,不让这个事件再向下传递了.
那么这个一定的条件是什么呢?
mTouchPointDist.x = ev.getRawX() - mTouchPoint.x;mTouchPointDist.y = ev.getRawY() - mTouchPoint.y;//只负责起始时的拦截.拦截后,事件就完全交给touch来处理了.if (Math.abs(mTouchPointDist.y) > Math.abs(mTouchPointDist.x)) break;if (mTouchPointDist.x < MIN_SLOP) break;return true;
代码中我们可以看到,采取放行策略,即哪些条件可以放行此事件,反之其他的条件都要拦截.那么哪些条件可以放行呢? 当垂直方向的移动距离大于水平方向时,此时我们可能期望的是界面中的ListView的上下滚动,而不是界面随手指移动,因此放行. 第二条件,当水平移动距离小于一个最少距离(24DP)时,也放行,为什么呢?因为手指触摸到屏幕时,由于触摸面积的问题,微小的动作都为引起此MOVE事件,如果因为手指轻轻碰一下屏幕就产生位移,会有太灵敏的感觉,体验绝对不好.因此不拦截.
一旦拦截到事件后,剩下的MOVE事件就会全部发给自身的onTouchEvent().这样在onTouch中让手指跟随界面移动.即可.于是代码如下:
@Override public boolean onTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_MOVE: { mTouchPointDist.x = ev.getRawX() - mTouchPoint.x; mTouchPointDist.y = ev.getRawY() - mTouchPoint.y; //负责滑动距离的计算. if (getScrollX() - mTouchPointDist.x > 0) { mTouchPointDist.x = getScrollX(); } mTouchPoint.x = ev.getRawX(); mTouchPoint.y = ev.getRawY(); onGestureListener.onScroll(mTouchPointDist.x, mTouchPointDist.y); onGestureListener.onFling(mTouchPointDist.x * 1000 / (ev.getEventTime() - eventTime), mTouchPointDist.y * 1000 / (ev.getEventTime() - eventTime)); eventTime = ev.getEventTime(); break; } case MotionEvent.ACTION_UP: { onGestureListener.onUp(); break; } } return super.onTouchEvent(ev); }
同样是检测MOVE事件,只不过这里不是做拦截,而是计算滑动距离,然后让界面跟着手指左右移动.当然要做好边界检测,控制界面只能向左滑到0,向右滑到getWidth()
最后在松开手指的时候,检测滑动的距离是否超过半屏,从而用属性动画的方式,控制界面是退回原位0还是继续向右滑动最后消失.当界面消失时,我们调用了
@Override public void onAnimationEnd(Animator animation) { Context c = getContext(); if (c instanceof Activity) { ((Activity) c).onBackPressed(); } }
Activity.onBackPressed().默认的实现就是finish界面.如果你重写了activity的onBackPressed().那么一定记得带上finish().否则此组件无法实现关闭界面的效果,只是将界面移出了屏幕.而没有destroy()界面.
当然你也可以修改此组件,让这个关闭变成一个回调的过程,这样你就可以在回调中实现关闭动作,而不必依赖Activity的onBackPressed()功能.
还有最后一个小的技巧,我们将界面滑离原来的位置时,发现左边边界与底部颜色融合到一起了,没有层次分明的效果,于是想到要在左边边缘加上阴影效果,很可惜的是,android的View没有阴影的属性,只能自行脑补一个阴影实现.我的思路如下:
在HDraggableLayout的左边加入一个带阴影效果的自定义View.让它刚好偏离出屏幕,当我们滑动的时候,就可以把这个阴影拖出来了.
阴影View的实现如下:
//实现一个带阴影的控件.阴影呈现在左边.class ShadowView extends View { private Paint shadowPaint = new Paint(); public ShadowView(Context context) { super(context); } @Override protected void onDraw(Canvas canvas) { Shader mShader = new LinearGradient(getWidth(), 0, 0, 0, new int[]{Color.GRAY, Color.TRANSPARENT}, null, Shader.TileMode.CLAMP); shadowPaint.setShader(mShader); canvas.drawRect(0, 0, getWidth(), getHeight(), shadowPaint); }}
将阴影加入Layout的代码如下:
public HDraggableLayout(Context context) { super(context); View shadowView = new ShadowView(context); LayoutParams p = new LayoutParams(20, ViewGroup.LayoutParams.MATCH_PARENT); p.gravity = Gravity.LEFT | Gravity.TOP; p.leftMargin = -20; addView(shadowView, p); }
有兴趣可以改造此组件,可以增加改变阴影颜色,阴影宽度等方法.
一切就这么简单.一个能够将你的Activity变成可滑动删除的组件就写好了.
实际测试的时候发现,Activity的默认背景颜色为黑色,而不是透明的.想在代码中直接给Activity设置一个透明的样式无论如何都没有起效果.于是采用了最笨的办法,在Manifest.xml中直接给Activity设置一个背景透明的Theme样式.样式如下:
<style name="myTransparent" parent="AppBaseTheme"> <item name="android:windowBackground">@android:color/transparent</item> <item name="android:windowIsTranslucent">true</item> <item name="android:windowAnimationStyle">@android:style/Animation.Translucent</item> </style>
大功告成!看看效果吧.
- Android自制滑动删除Activity组件
- Android滑动删除activity
- vue滑动删除组件
- Android自定义View之仿知乎滑动删除Activity
- Android滑动切换activity
- Android ListView滑动删除
- android,listView, 滑动删除
- android listview滑动删除
- Android ListView 滑动删除
- Android滑动删除控件
- Android滑动删除ListView
- SwipeListView滑动删除Android
- Android-滑动删除布局
- Android 滑动删除viewGroup
- android滑动删除listview
- Android的Activity组件
- android组件之Activity
- Android之Activity组件
- Web项目的前台兼容性问题——代码规范篇
- HTTP请求响应过程
- Leetcode239: Dungeon Game
- Codeforces Round #339 (Div. 2) B.Gena's Code
- 数据库,你真的懂他吗?
- Android自制滑动删除Activity组件
- hdoj 5249 KPI 【STL】
- 导入csv数据到mongodb中出现问题:exception:Invalid UTF8 character detected
- Android开发出现Warning:Gradle version 2.10 is required. Current version is 2.8. If u
- 《你是我的眼》,歌曲很好听
- 格式化数据#3:有关逻辑推理/语义的资源
- 深度学习 和 tensorflow 学习资料收集
- 池化方法总结
- 【bzoj2242】[SDOI2011]计算器 快速幂+exgcd+BSGS