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>

大功告成!看看效果吧.
这里写图片描述

0 0
原创粉丝点击