自定义控件之侧滑关闭 Activity 控件

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zgcqflqinhao/article/details/79168799

隔壁 iOS 的小伙伴有一个功能就是左手向右手一个慢动作,轻轻一划就可以关闭界面,这种操作感觉还是很丝滑的,而且这还是 iOS 系统自带的功能,由于 Android 手机早期是有 back 键,home 键 和菜单键(现在大部分手机都只保留一个键了),所以 Android 是没有这个功能的。现在用户越来越注重体验,一般为了降低设计成本,在 App 的设计上 iOS 与 Android 也力求风格统一,那么如果需要我们也实现这样的功能怎么办?程序猿可是不会被难倒的一个物种,有很多这样功能的开源控件,目前公司项目也有用到,但是用起来却是有点问题,于是决定自己试着实现一下,也是一种学习。

1 思路

首先,侧滑这个动作并不难,我们监听到一个控件的触摸事件,然后改变它的横纵坐标即可(这个我之前有写过可以自由移动的控件,http://blog.csdn.net/zgcqflqinhao/article/details/72731633,准备重新改一下这个控件,但是原理是一样的),问题是我们应该监听哪个控件的触摸。我们可以获取到 Activity 的根视图,于是我就想着把这个根视图再放到一个自定义的容器上,那样就好处理了。然后除了侧滑的效果,我们还得处理它的事件冲突(鄙人对事件冲突也简单学习过 http://blog.csdn.net/zgcqflqinhao/article/details/72110352),毕竟 Android 中可以滑动的控件还不少,比如 ViewPager、SeekBar 等等。OK,既然有思路了,那就可以开始我们的光荣之路了。

2 侧滑的基本实现

public class SlideBackLayout extends FrameLayout {
    private boolean startSlide = false;
    private float lastX;
    private Activity activity;

    public SlideBackLayout(@NonNull Context context) {
        this(context, null);
    }

    public SlideBackLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SlideBackLayout(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (event.getX() < getMeasuredWidth() / 10) {
                    startSlide = true;
                    lastX = event.getRawX();
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (startSlide) {
                    float distanceX = event.getRawX() - lastX;
                    float nextX = getX() + distanceX;
                    setX(nextX);
                    lastX = event.getRawX();
                }
                break;
            case MotionEvent.ACTION_UP:
                if (startSlide && event.getRawX() > getMeasuredWidth() / 2) {
                    setX(getMeasuredWidth());
                    //Finish activity
                    activity.finish();
                } else {
                    setX(0);
                }
                startSlide = false;
                break;
        }
        return true;
    }

    public void bindActivity(Activity activity) {
        this.activity = activity;
        ViewGroup mDecorView = (ViewGroup) activity.getWindow().getDecorView();
        View mRootView = mDecorView.getChildAt(0);
        mDecorView.removeView(mRootView);
        addView(mRootView);
        mDecorView.addView(this);
    }

    public void unbindActivity() {
        this.activity = null;
    }
}

在需要侧滑关闭的 Activity 中(一般会在 BaseActivity 中)添加如下代码(在 setContentView() 方法后调用就行,其他地方暂时还未测试):

        SlideBackLayout mSlideBackLayout = new SlideBackLayout(this);
        mSlideBackLayout.bindActivity(this);

3 拦截子 View 的触摸事件

可以看到我们实现了基本的侧滑操作了,滑完后未超过一半自动回到原来的样子,超过一半则关闭 Activity,这中间我还点了一下按钮,不是我手贱,是想说明子控件的点击事件依然可以响应,但是这就有个问题了,如果这个控件的宽高全屏了,那就没有办法执行侧滑动作了。如下图:

这是因为子 View 把我们的触摸事件给消费了,那么应该在适当的时机来拦截一下触摸事件,让我们侧滑控件自己消费而不下发到子 View,重写 onInterceptTouchEvent() 方法,当触摸点的横坐标小于屏幕宽度十分之一时就不下发触摸事件:

    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (event.getX() < getMeasuredWidth() / 10) {
                    startSlide = true;
                    return true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return super.onInterceptTouchEvent(event);
    }

目前效果如下:

对了,这里还有一个 Bug,如果调用了 bindActivity() 再调用 unbindActivity() 方法,侧滑操作仍会执行,关闭的时候就会由于 activity 为 null 而报 NullPointException,所以我们在拦截触摸事件和响应触摸事件的时候先进行非空判断:

        if (activity == null) {
            return super.onInterceptTouchEvent(event);
        }
        if (activity == null) {
            return super.onTouchEvent(event);
        }

到目前为止,基本的侧滑我们能实现了,对子 View 触摸事件的拦截我们也做了一些处理。

4 处理滑动冲突

接下来我们要处理一些控件的滑动冲突,左右滑动的控件最典型的也就是 ViewPager 了,目前公司使用的侧滑关闭菜单中也是只处理了 ViewPager 的滑动冲突,如果不处理冲突,那么在存在 ViewPager 的时候,无论 ViewPager 当前处于第几个页卡,只要我们在小于屏幕宽度十分之一的地方开始滑动,那就会响应侧滑事件而不是 ViewPager 的滑动,我们希望的应该是在 ViewPager 处于非第一个页卡时,先响应 ViewPager 的滑动,ViewPager 处于第一个页卡时才响应侧滑关闭事件。那么首先需要一个容器来存放当前布局中存在的 ViewPager,然后检查到当前布局中有 ViewPager 的时候就存到容器中,这样我们在处理触摸事件的时候再根据有没有 ViewPager 和 ViewPager 当前页卡是第几项来绝对如何处理触摸事件。

检查当前布局是否有 ViewPager 这个方法在绑定 Activity 的时候调用:

    private void checkHasViewPager(ViewGroup viewGroup) {
        int childCount = viewGroup.getChildCount();
        for (int i = 0; i < childCount; i++) {
            if (viewGroup.getChildAt(i) instanceof ViewPager) {
                viewPagerList.add((ViewPager) viewGroup.getChildAt(i));
            } else if (viewGroup.getChildAt(i) instanceof ViewGroup) {
                checkHasViewPager((ViewGroup) viewGroup.getChildAt(i));
            }
        }
    }

然后在 onInterceptTouchEvent() 方法中处理事件之前加入对 ViewPager 的处理:

        if (!viewPagerList.isEmpty()) {
            for (int i = 0; i < viewPagerList.size(); i++) {
                if (viewPagerList.get(i).getCurrentItem() != 0) {
                    return super.onInterceptTouchEvent(event);
                }
            }
        }

这样处理过后就可以看到即使在小于屏幕宽度十分之一的地方滑动时也是先响应 ViewPager 的触摸事件,直到处于第一个页卡才开始侧滑关闭。细心的你会发现我又自作主张的在界面顶部加上了一个 SeekBar,而且当我想滑动 SeekBar 时并没有成功,而是响应了侧滑事件,其实这就是我为什么想写这个控件的起因。我希望我可以自定义某些控件也跟 ViewPager 一样不被拦截触摸事件,所以我还加入了一个存放不想被拦截事件的容器。

然后给外部提供一个方法,可以添加希望不被拦截触摸事件的 View:

    public void addNotInterceptView(View view) {
        notInterceptViewList.add(view);
    }

onInterceptTouchEvent() 方法中处理完 ViewPager 就可以处理这些 View 了:

        if (!notInterceptViewList.isEmpty()) {
            for (int i = 0; i < notInterceptViewList.size(); i++) {
                View mView = notInterceptViewList.get(i);
                int[] location = new int[2];
                mView.getLocationOnScreen(location);
                if (event.getX() >= location[0] && event.getX() <= location[0] + mView.getWidth()
                        && event.getY() >= location[1] && event.getY() <= location[1] + mView.getHeight()) {
                    return super.onInterceptTouchEvent(event);
                }
            }
        }

当我们希望 SeekBar 的触摸事件不被拦截时,就可以调用 addNotInterceptView() 方法:

        SeekBar sbTest = (SeekBar) findViewById(R.id.sb_test);
        mSlideBackLayout.addNotInterceptView(sbTest);

现在效果如下:

5 总结

这个控件在使用时仍然和其他的控件一样,必须设置 Activity 的主题为透明主题,继承 Activity 时可以使用

@android:style/Theme.Translucent.NoTitleBar

继承 AppCompat 时需自定义主题,然后在自定义主题中加入如下两行:

        <!-- 透明背景 -->
        <item name="android:windowBackground">@android:color/transparent</item>
        <!-- 设置是否透明 -->
        <item name="android:windowIsTranslucent">true</item>

这次的自定义侧滑关闭 Activity 控件比起现在公司用的多了两个功能,一是取消 Activity 的绑定,因为有的 Activity 并不希望有这个功能,二是可以添加自定义的希望不被拦截触摸事件的 View。当然我现在还没有大量测试这个控件的稳定性,如果有发现问题,欢迎留言。

6 Github 传送门

https://github.com/mrqinshou/SlideBackLayoutDemo

猜你喜欢

转载自blog.csdn.net/zgcqflqinhao/article/details/79168799