为您推荐:
精华内容
最热下载
问答
  • 5星
    69.08MB weixin_43474701 2021-07-19 18:57:32
  • 64KB weixin_38641111 2020-08-31 13:59:47
  • 17.34MB u010090644 2020-04-02 11:00:41
  • 857KB fcw_one 2018-09-21 17:11:57
  • 11.88MB u014137988 2016-08-02 19:28:06
  • 5星
    19.75MB wjilikely 2016-07-25 17:03:31
  • 5星
    6KB lthcth111 2016-08-17 09:16:48
  • 7.86MB bgc525725278 2014-09-15 15:12:52
  • 654KB zl_peng 2016-08-24 16:15:18
  • 4.54MB qq_28527551 2017-12-26 17:50:13
  • 5星
    21.03MB aa_chao 2016-09-20 16:32:49
  • Android滑动开关,只需一个类 项目中经常会用到滑动开关,但是又不喜欢引入别的lib,个人比较喜欢一个类能搞定且容易移植的东西,所以写了一个滑动开关类,虽然网上有很多demo了,但是还是想自己写一个,巩固记忆并且...

    Android滑动开关,只需一个类

    项目中经常会用到滑动开关,但是又不喜欢引入别的lib,个人比较喜欢一个类能搞定且容易移植的东西,所以写了一个滑动开关类,虽然网上有很多demo了,但是还是想自己写一个,巩固记忆并且练习一下Scroller的使用,下面把效果图和代码放进来希望帮助到有需要的人~


    效果图

    这里写图片描述

    使用方法

    1.设置小圆模式

    调用:setSmallCircleModel()方法
      /**
         * * 设置小圆模式
         *
         * @param strokeLineColor      圆角矩形的边颜色
         * @param strokeSolidColor     圆角矩形的填充颜色
         * @param circleCheckedColor   内部小圆被选中的颜色
         * @param circleNoCheckedColor 内部小圆未被选中的颜色
         */
        public void setSmallCircleModel(int strokeLineColor, int strokeSolidColor, int circleCheckedColor, int circleNoCheckedColor) {
        }

    2.设置大圆模式

    调用:setBigCircleModel()方法
     /**
         * 设置大圆模式
         *
         * @param strokeLineColor           圆角矩形边线颜色
         * @param strokeCheckedSolidColor   圆角矩形选择状态下的填充颜色
         * @param strokeNoCheckedSolidColor 圆角矩形非选择状态下填充颜色
         * @param circleChecked             滑动圆选择状态下的填充颜色
         * @param circleNoCheckColor        滑动圆非选中状态下的填充颜色
         */
        public void setBigCircleModel(int strokeLineColor, int strokeCheckedSolidColor,
                                      int strokeNoCheckedSolidColor, int circleChecked,
                                      int circleNoCheckColor) {}

    3.设置按钮开关监听方法

    调用 :setOnCheckedListener 方法
     /**
         * 设置点击监听
         *
         * @param listener
         */
        public void setOnCheckedListener(SlideButtonOnCheckedListener listener) {
            this.mListener = listener;
        }

    4.设置按钮状态

    调用:setChecked方法
        /**
         * 设置按钮状态
         *
         * @param checked
         */
        public void setChecked(boolean checked) {
            this.isChecked = checked;
            if (isChecked) {
                circle_x = circleEndX;
            } else {
                circle_x = circleStartX;
            }
            invalidate();
        }

    Activity中使用方法

       SlideButton button = findViewById(R.id.button1);
            button.setSmallCircleModel(parseColor("#cccccc"), parseColor("#00000000"), parseColor("#FF4040"), parseColor("#cccccc"));
            button.setOnCheckedListener(this);
    
            SlideButton button2 = findViewById(R.id.button2);
            button2.setBigCircleModel(parseColor("#cccccc"), parseColor("#FF4040"), parseColor("#00ffffff"), parseColor("#ffffff"), parseColor("#ffffff"));
    

    完整代码类:SlideButton

    package com.gh.slidebutton;
    
    import android.content.Context;
    import android.graphics.Canvas;
    import android.graphics.Color;
    import android.graphics.Paint;
    import android.support.annotation.Nullable;
    import android.util.AttributeSet;
    import android.util.Log;
    import android.view.MotionEvent;
    import android.view.View;
    import android.widget.Scroller;
    
    /**
     * Created by Administrator on 2018/5/14/014.
     */
    
    public class SlideButton extends View {
    
    
        //状态改变监听
        public interface SlideButtonOnCheckedListener {
            void onCheckedChangeListener(boolean isChecked);
        }
    
        private SlideButtonOnCheckedListener mListener;
    
        //view默认的高,view默认的宽是高的两倍(单位:dp)
        public static final int VIEW_HEIGHT = 20;
        //椭圆的边框宽度
        private static final int strokeLineWidth = 3;
        //圆的边框宽度
        private static final int circleStrokeWidth = 3;
    
        //椭圆边框颜色
        private String StrokeLineColor = "#bebfc1";
        //椭圆填充颜色
        private String StrokeSolidColor = "#00ffffff";
        //圆形边框颜色
        private String CircleStrokeColor = "#abacaf";
        //圆形checked填充颜色
        private String CircleCheckedColor = "#ff5555";
        //圆形非checked填充颜色
        private String CircleNoCheckedColor = "#bebfc1";
    
        //控件内边距
        private static int PADDING = 20;
        //移动的判定距离
        private static int MOVE_DISTANCE = 50;
    
        //圆的x轴圆心
        private float circle_x;
    
        //是否是大圆
        private boolean isBigCircle = false;
    
        //圆角矩形的高
        private int strokeHeight;
        //圆角矩形的半径
        private float strokeCircleRadius;
        //内部圆的半径
        private float circleRadius;
        private Scroller mScroller;
        //当前按钮的开关状态
        private boolean isChecked = false;
    
        private int mWidth;
        private int mHeight;
    
        private Paint mPaint;
        private float circleStartX;
        private float circleEndX;
        private int centerX;
        private int centerY;
        private float preX = 0;
        private boolean isMove;
        private int view_height_int;
        private int strokeLineColor_int;
        private int strokeCheckedSolidColor_int;
        private int strokeNoCheckedSolidColor_int;
        private int circleStrokeColor_int;
        private int circleChecked_int;
        private int circleNoCheckedColor_int;
    
        public SlideButton(Context context) {
            super(context);
            init(context);
        }
    
        public SlideButton(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            init(context);
        }
    
        public SlideButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init(context);
        }
    
        /**
         * * 设置小圆模式
         *
         * @param strokeLineColor      圆角矩形的边颜色
         * @param strokeSolidColor     圆角矩形的填充颜色
         * @param circleCheckedColor   内部小圆被选中的颜色
         * @param circleNoCheckedColor 内部小圆未被选中的颜色
         */
        public void setSmallCircleModel(int strokeLineColor, int strokeSolidColor, int circleCheckedColor, int circleNoCheckedColor) {
            isBigCircle = false;
            strokeLineColor_int = strokeLineColor;
            strokeNoCheckedSolidColor_int = strokeSolidColor;
            circleChecked_int = circleCheckedColor;
            circleNoCheckedColor_int = circleNoCheckedColor;
            invalidate();
        }
    
        /**
         * 设置大圆模式
         *
         * @param strokeLineColor           圆角矩形边线颜色
         * @param strokeCheckedSolidColor   圆角矩形选择状态下的填充颜色
         * @param strokeNoCheckedSolidColor 圆角矩形非选择状态下填充颜色
         * @param circleChecked             滑动圆选择状态下的填充颜色
         * @param circleNoCheckColor        滑动圆非选中状态下的填充颜色
         */
        public void setBigCircleModel(int strokeLineColor, int strokeCheckedSolidColor,
                                      int strokeNoCheckedSolidColor, int circleChecked,
                                      int circleNoCheckColor) {
            isBigCircle = true;
            strokeLineColor_int = strokeLineColor;
            strokeCheckedSolidColor_int = strokeCheckedSolidColor;
            strokeNoCheckedSolidColor_int = strokeNoCheckedSolidColor;
            circleChecked_int = circleChecked;
            circleNoCheckedColor_int = circleNoCheckColor;
            invalidate();
        }
    
        /**
         * 设置点击监听
         *
         * @param listener
         */
        public void setOnCheckedListener(SlideButtonOnCheckedListener listener) {
            this.mListener = listener;
        }
    
        /**
         * 设置按钮状态
         *
         * @param checked
         */
        public void setChecked(boolean checked) {
            this.isChecked = checked;
            if (isChecked) {
                circle_x = circleEndX;
            } else {
                circle_x = circleStartX;
            }
            invalidate();
        }
    
        private void init(Context context) {
            setEnabled(true);
            setClickable(true);
            mPaint = new Paint();
            mScroller = new Scroller(context);
            view_height_int = dip2px(context, VIEW_HEIGHT);
            strokeLineColor_int = Color.parseColor(StrokeLineColor);
            strokeNoCheckedSolidColor_int = Color.parseColor(StrokeSolidColor);
            circleStrokeColor_int = Color.parseColor(CircleStrokeColor);
            circleChecked_int = Color.parseColor(CircleCheckedColor);
            circleNoCheckedColor_int = Color.parseColor(CircleNoCheckedColor);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int widthMode = MeasureSpec.getMode(widthMeasureSpec);
            int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    
            int heightMode = MeasureSpec.getMode(heightMeasureSpec);
            int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    
            if (heightMode == MeasureSpec.AT_MOST) {
                //如果是wrap_content
                heightSize = view_height_int;
            }
            if (widthMode == MeasureSpec.AT_MOST) {
                widthSize = heightSize * 2;
            }
            setMeasuredDimension(widthSize, heightSize);
        }
    
        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            mWidth = w;
            mHeight = h;
            if (isBigCircle) {
                PADDING = h / 10;
            } else {
                PADDING = h / 15;
            }
            MOVE_DISTANCE = mWidth / 100;
            //圆角椭圆的高
            strokeHeight = h - PADDING * 2;
            //外部圆角矩形的半径
            strokeCircleRadius = strokeHeight / 2;
            centerY = mHeight / 2;
            //内部圆的半径
            if (isBigCircle) {
                circleRadius = strokeCircleRadius + PADDING;
            } else {
                circleRadius = strokeCircleRadius - PADDING * 2;
            }
            Log.i("TAG", "mHeight:" + mHeight + "   strokeCircleRadius: " + strokeCircleRadius);
            //内部圆的x轴起始坐标
            circleStartX = PADDING + strokeCircleRadius;
            //内部圆的x轴终点坐标
            circleEndX = mWidth - circleStartX;
            if (isChecked) {
                circle_x = circleEndX;
            } else {
                circle_x = circleStartX;
            }
    
            //控件的中线
            centerX = mWidth / 2;
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            drawRect(canvas);
            drawCircle(canvas);
        }
    
        //画圆角矩形
        private void drawRect(Canvas canvas) {
            mPaint.reset();
            mPaint.setAntiAlias(true);
            mPaint.setDither(true);
    
            if (isBigCircle && isChecked) {
                mPaint.setColor(strokeCheckedSolidColor_int);
            } else {
                mPaint.setColor(strokeNoCheckedSolidColor_int);
            }
            //画填充
            canvas.drawRoundRect(PADDING, PADDING, mWidth - PADDING, mHeight - PADDING, strokeCircleRadius, strokeCircleRadius, mPaint);
    
            //画边框
            mPaint.setStrokeWidth(strokeLineWidth);
            mPaint.setColor(strokeLineColor_int);
            mPaint.setStyle(Paint.Style.STROKE);
            canvas.drawRoundRect(PADDING, PADDING, mWidth - PADDING, mHeight - PADDING, strokeCircleRadius, strokeCircleRadius, mPaint);
        }
    
        //画里面的圆
        private void drawCircle(Canvas canvas) {
            mPaint.reset();
            mPaint.setAntiAlias(true);
            mPaint.setDither(true);
            float circleRadiusNew = circleRadius;
            if (isBigCircle) {
                circleRadiusNew -= circleStrokeWidth;
            }
            if (isChecked) {
                mPaint.setColor(circleChecked_int);
            } else {
                mPaint.setColor(circleNoCheckedColor_int);
            }
            canvas.drawCircle(circle_x, centerY, circleRadiusNew, mPaint);
    
            if (isBigCircle) {
                //画圆的边
                mPaint.setColor(circleStrokeColor_int);
                mPaint.setStyle(Paint.Style.STROKE);
                mPaint.setStrokeWidth(circleStrokeWidth);
                canvas.drawCircle(circle_x, centerY, circleRadiusNew, mPaint);
            }
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    preX = event.getX();
                    isMove = false;
                    if (!isChecked) {
                        circle_x = PADDING + strokeCircleRadius;
                    } else {
                        circle_x = mWidth - PADDING - strokeCircleRadius;
                    }
                    break;
                case MotionEvent.ACTION_MOVE:
                    float move_x = event.getX();
                    if (Math.abs(move_x - preX) > MOVE_DISTANCE) {
                        isMove = true;
                        if (move_x < circleStartX) {
                            circle_x = circleStartX;
                            isChecked = false;
                        } else if (move_x > circleEndX) {
                            circle_x = circleEndX;
                            isChecked = true;
                        } else {
                            circle_x = move_x;
                        }
                        invalidate();
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    if (isMove) {
                        if (circle_x >= centerX) {
                            //关闭(执行开启)
                            mScroller.startScroll((int) circle_x, 0, (int) (circleEndX - circle_x), 0);
                            isChecked = true;
                        } else {
                            //开启(执行关闭)
                            mScroller.startScroll((int) circle_x, 0, (int) (circleStartX - circle_x), 0);
                            isChecked = false;
                        }
                    } else {
                        if (!isChecked) {
                            //关闭(执行开启)
                            mScroller.startScroll((int) circle_x, 0, (int) (circleEndX - circle_x), 0);
                            isChecked = true;
                        } else {
                            //开启(执行关闭)
                            mScroller.startScroll((int) circle_x, 0, (int) (circleStartX - circle_x), 0);
                            isChecked = false;
                        }
                    }
                    if (mListener != null) {
                        mListener.onCheckedChangeListener(isChecked);
                    }
                    invalidate();
                    break;
            }
            return super.onTouchEvent(event);
        }
    
        @Override
        public void computeScroll() {
            if (mScroller.computeScrollOffset()) {
                circle_x = mScroller.getCurrX();
                invalidate();
            }
        }
    
        /**
         * 根据手机的分辨率从 dp 的单位 转成为 px(像素)
         */
        public static int dip2px(Context context, float dpValue) {
            final float scale = context.getResources().getDisplayMetrics().density;
            return (int) (dpValue * scale + 0.5f);
        }
    }
    
    展开全文
    sinat_26708935 2018-05-24 16:30:24
  • 762KB weixin_39840914 2019-08-13 04:10:57
  • 189KB weixin_38631329 2020-08-31 19:46:50
  • 这种方式的滑动感应范围是整个布局。 private void getTouchView(){ // 获取布局文件 LayoutInflater inflater = (LayoutInflater) mContext .getSystemService(Context.LAYOUT_INFLATER_SERVICE); vie...

    1.获取布局文件或视图View

    1. 获取布局文件
      这种方式的滑动感应范围是整个布局。
    private void getTouchView(){
    	// 获取布局文件
    	LayoutInflater inflater = (LayoutInflater) mContext
            .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    	view = inflater.inflate(R.layout.main, null);
    	// 设置滑动监听
    	setOnLayoutTouchListener();
    }
    
    1. 获取视图View
      这种方式的滑动感应范围是View区域。
    private void getTouchView(){
    	rv_informations = rootView.findViewById(R.id.rv_informations);
    	// 设置滑动监听
    	setOnViewTouchListener();
    }
    

    2.设置滑动监听事件

    1. 获取布局文件
    private void setOnLayoutTouchListener(){
    	view.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    switch (event.getAction()){
                        case MotionEvent.ACTION_DOWN:
                            posX = event.getX();
                            posY = event.getY();
                            break;
                        case MotionEvent.ACTION_MOVE:
                            curPosX = event.getX();
                            curPosY = event.getY();
                            break;
                        case MotionEvent.ACTION_UP:
                            if ((curPosX - posX > 0) && (Math.abs(curPosX - posX) > 25)){
                                Log.v(TAG,"向左滑动");
                            }
                            else if ((curPosX - posX < 0) && (Math.abs(curPosX-posX) > 25)){
                                Log.v(TAG,"向右滑动");
                            }
                            if ((curPosY - posY > 0) && (Math.abs(curPosY - posY) > 25)){
                                Log.v(TAG,"向下滑动");
                            }
                            else if ((curPosY - posY < 0) && (Math.abs(curPosY-posY) > 25)){
                                Log.v(TAG,"向上滑动");
                            }
                            break;
                    }
                    return true;
                }
            });
    }
    
    1. 获取视图View
    private void setOnViewTouchListener(){
    	rv_informations.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    switch (event.getAction()){
                        case MotionEvent.ACTION_DOWN:
                            posX = event.getX();
                            posY = event.getY();
                            break;
                        case MotionEvent.ACTION_MOVE:
                            curPosX = event.getX();
                            curPosY = event.getY();
                            break;
                        case MotionEvent.ACTION_UP:
                            if ((curPosX - posX > 0) && (Math.abs(curPosX - posX) > 25)){
                                Log.v(TAG,"向左滑动");
                            }
                            else if ((curPosX - posX < 0) && (Math.abs(curPosX-posX) > 25)){
                                Log.v(TAG,"向右滑动");
                            }
                            if ((curPosY - posY > 0) && (Math.abs(curPosY - posY) > 25)){
                                Log.v(TAG,"向下滑动");
                            }
                            else if ((curPosY - posY < 0) && (Math.abs(curPosY-posY) > 25)){
                                Log.v(TAG,"向上滑动");
                            }
                            break;
                    }
                    return true;
                }
            });
    }
    

    3.调试截图

    测试结果

    展开全文
    gengkui9897 2019-02-25 21:15:20
  • 本文接上篇Android滑动冲突一内部拦截外部拦截简介 一 ViewPager嵌套ListView的滑动冲突,内部拦截法为何ViewPager的onInterceptTouchEvent要做判断而不是直接返回true? 我们重温下Android事件分发二之...

     

    Android事件分发流程源码解析一

    Android事件分发流程源码解析二及总结

    Android滑动冲突解决方案内外部拦截法及原理

    在前面阐述了事件分发的整个流程,那么如何来解决常见的滑动冲突呢,本文首先总结解决滑动冲突的方式,再结合实际例子阐述如何运用。

    一 滑动冲突的两种解决方式

    1.1 外部拦截法 主要代码重写父容器的事件拦截方法

        private int mLastXIntercept;
        private int mLastYIntercept;
        @Override
        public boolean onInterceptTouchEvent(MotionEvent event) {
    
            int x = (int) event.getX();
            int y = (int) event.getY();
            final int action = event.getAction();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    mLastXIntercept = (int) event.getX();
                    mLastYIntercept = (int) event.getY();
                    break;
                case MotionEvent.ACTION_MOVE:
          
                    if (needIntercept) {//判断是否需要拦截的条件
                        return true;
                    }
    
                    break;
                case MotionEvent.ACTION_UP:
                    break;
            }
    
    
            return super.onInterceptTouchEvent(event);
        }

     

    1.2 内部拦截法 主要代码重写子容器的事件分发方法和父容器的事件拦截方法

    • 父容器事件拦截
      @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            final int action = ev.getAction() & MotionEvent.ACTION_MASK;
            if (action == MotionEvent.ACTION_DOWN){
                super.onInterceptTouchEvent(ev);
                return false;
            }
            return true;
    
        }
    • 子容器事件分发
      public boolean dispatchTouchEvent(MotionEvent event) {
            int x = (int) event.getX();
            int y = (int) event.getY();
    
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN: {
                    parent.requestDisallowInterceptTouchEvent(true);
                    break;
                }
                case MotionEvent.ACTION_MOVE: {
                    int deltaX = x - mLastX;
                    int deltaY = y - mLastY;
                    if (父容器需要此类点击事件) {
                        parent.requestDisallowInterceptTouchEvent(false);
                    }
                    break;
                }
                case MotionEvent.ACTION_UP: {
                    break;
                }
                default:
                    break;
            }
    
            mLastX = x;
            mLastY = y;
            return super.dispatchTouchEvent(event);
        }

    二 何为滑动冲突,何时会产生滑动冲突?

    • 系统不知将滑动事件交给谁来消费

    不同方向的滑动冲突

    ViewPager里面嵌套了一个ListView,手指在屏幕上面滑动的时候,系统不知道把这个滑动给ViewPager还是ListView。当然ViewPager源码对滑动方向做了判断,在左右滑动的时候让ViewPager直接拦截事件自己消费,其它情况不拦截交给子View处理,所以在我们日常使用的时候才不会产生冲突。

    同一方向的滑动冲突

    比如ScrollView里面嵌套了一个ScrollView,都是竖直方向滑动。此时该交给哪个ScrollView来消费事件呢?

    三 实际应用

    利用ViewPager里面嵌套ListView来做解析。由于ViewPager本身做了滑动冲突的处理,这里为了演示,我们重写了ViewPager--》BadViewPager

     场景: activity里面放一个ViewPager,ViewPager每页里面为一个为ListView或者TexView

    activity的代码,主要就是设置ViewPager每页的为一个一个ListView或者TextView(根据initData里面的参数决定)

        private BadViewPager mViewPager;
        private List<View> mViews;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_viewpager);
            initViews();
            initData(false);
        }
    
        protected void initViews() {
    
            mViewPager = findViewById(R.id.view_pager);
            mViews = new ArrayList<>();
        }
    
        protected void initData(final boolean isListView) {
       
            Flowable.just("view1", "view2", "view3", "view4").subscribe(new Consumer<String>() {
                @Override
                public void accept(String s) throws Exception {
                    //当前View
                    View view;
                    if (isListView) {
                          MyListView listView = new MyListView(MainActivity.this);
                        final ArrayList<String> datas = new ArrayList<>();
                        Flowable.range(0, 70).subscribe(new Consumer<Integer>() {
                            @Override
                            public void accept(Integer integer) throws Exception {
                                datas.add("data" + integer);
                            }
                        });
                        ArrayAdapter<String> adapter = new ArrayAdapter<>
                                (MainActivity.this, android.R.layout.simple_list_item_1, datas);
                        listView.setAdapter(adapter);
                        view = listView;
                    } else {
                        //初始化TextView
                        TextView textView = new TextView(MainActivity.this);
                        textView.setGravity(Gravity.CENTER);
                        textView.setText(s);
    //                    textView.setOnClickListener(new View.OnClickListener() {
    //                        @Override
    //                        public void onClick(View v) {
    //
    //                        }
    //                    });
    //                    Button textView = new Button(MainActivity.this);
    //                    textView.setText(s);
                        view = textView;
                    }
                    //将当前View添加到ViewPager的ViewList中去
                    mViews.add(view);
                }
            });
            mViewPager.setAdapter(new BasePagerAdapter(mViews));
        }

    这里用了RxJava

    implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'
    implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'

    activity_viewpager很简单

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout 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"
        android:orientation="vertical"
        tools:context=".MainActivity">
    <com.example.learn_dispatch.BadViewPager
        android:id="@+id/view_pager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
    
    </LinearLayout>
    MyListView:
    
    
    public class MyListView extends ListView {
    
        public MyListView(Context context) {
            super(context);
        }
    
        public MyListView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    }
    BasePagerAdapter:
    
    import android.view.View;
    
    import androidx.viewpager.widget.PagerAdapter;
    import androidx.viewpager.widget.ViewPager;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class BasePagerAdapter extends PagerAdapter {
    
        private List<View> views = new ArrayList<View>();
    
        public BasePagerAdapter(List<View> views) {
            this.views = views;
        }
    
        @Override
        public boolean isViewFromObject(View arg0,Object arg1){
            return arg0 == arg1;
        }
    
        @Override
        public int getCount(){
            return views.size ();
        }
    
        @Override
        public void destroyItem(View container,int position,Object object){
            ((ViewPager) container).removeView (views.get (position));
        }
    
        @Override
        public Object instantiateItem(View container,int position){
            ((ViewPager) container).addView (views.get (position));
            return views.get (position);
        }
    }

     

    3.1 场景一ViewPager的每页放的是TextView,其onInterceptTouchEvent事件拦截方法返回false不拦截

    重写ViewPager的onInterceptTouchEvent事件拦截方法返回false让其不拦截

    public class BadViewPager extends ViewPager {
    
    
        public BadViewPager(@NonNull Context context) {
            super(context);
        }
    
        public BadViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
        }
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            return  false;
        }
    }

    这种情况ViewPager是否可正常滑动呢?答案是可以

    在前面章节的事件分发当中,讲述了事件分发的整个流程。滑动事件是由上层ViewGroup一层层分发到最外面的View,如果没有消费再一层层返回给父级。显然这里,TextView没有消费事件将滑动事件交给其父ViewPager处理。

    那么为何没有消费呢?我们在前面的讲述了最终View是否消费取决于其onTouchEvent,是否记得在Android事件分发流程源码解析二及总结分析View的onTouchEvent源码这样一段话呢,只要CLICKABLE和LONG_CLICKABLE其中一个为true就会消费事件。通过查看源码我们知道TextView的CLICKABLE和LONG_CLICKABLE都为false,所以没有消费事件。这也就是ViewPager里面嵌套TextView,在ViewPager事件拦截为false的时候,能够正常滑动的原因。

    • 如果我们将TextView换为Button呢?大家可将上面activity里面代码注释掉的部分取消试试。

    结果是ViewPager不能滑动,为何不能呢?显然是Button消费了事件,没有交给ViewPager处理。我们知道Button的CLICKABLE为true默认是可点击的,所以消费了事件。

    • 比较有趣的是如果我们里面嵌套的还是一个TexView,但是给TexView添加了一个点击监听,是否可以正常滑动呢?

    通过测试ViewPager此时又不能正常滑动了,其实我们知晓了上面的事件分发原理,很容易推断出TextView消费了事件。肯定是我们在给TextView设置点击监听的时候改变了其是否可点击的值,我们查看其设置监听的源码

     public void setOnClickListener(@Nullable OnClickListener l) {
            if (!isClickable()) {
                setClickable(true);
            }
            getListenerInfo().mOnClickListener = l;
        }
    

     

    3.2 场景二ViewPager的每页放的是ListView

    ViewPager的onInterceptTouchEvent返回false,能否正常左右滑动呢?

    其实通过上面场景一的推断很容易得出结论。由于ListView把事件消费了,此时ViewPager不能左右滑动,LisView可以正常上下滑动

    那么如何来解决呢?此时就是需要我们的滑动冲突解决方案了

    • 外部拦截法

    举例中默认ViewPager的拦截onInterceptTouchEvent方法返回为false,不拦截。事件交给了ListView,ListView不消费才会返回给ViewPager。

    解决思路:在ViewPager分发事件的时候,让ViewPager先根据自己的情况进行判断,如果是自己的滑动手势就将事件拦截了自己处理,不是才交给ListView去处理。实际上我们查看ViewPager的源码也是这么处理的。

    我们重写ViewPager的拦截方法onInterceptTouchEvent,横向滑动时拦截事件,否则不做处理按正常流程分发。

     

    public class BadViewPager extends ViewPager {
    
    
        public BadViewPager(@NonNull Context context) {
            super(context);
        }
    
        public BadViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
        }
    
    
        private int mLastXIntercept;
        private int mLastYIntercept;
    //    外部拦截法
        @Override
    
        public boolean onInterceptTouchEvent(MotionEvent event) {
    
            int x = (int) event.getX();
            int y = (int) event.getY();
            final int action = event.getAction();
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    mLastXIntercept = (int) event.getX();
                    mLastYIntercept = (int) event.getY();
                    break;
                case MotionEvent.ACTION_MOVE:
                    //横坐标位移增量
                    int deltaX = x - mLastXIntercept;
                    //纵坐标位移增量
                    int deltaY = y - mLastYIntercept;
                    if (Math.abs(deltaX) > Math.abs(deltaY)) {
                        //左后滑动 拦截
                        return true;
                    }
                    break;
                case MotionEvent.ACTION_UP:
    
                    break;
            }
    
    
            return super.onInterceptTouchEvent(event);
        }
    }

    通过测试,这确实就解决了自己的ViewPager和LisView的滑动冲突,两者都可正常滑动

    • 内部拦截法

    思路:ViewPager默认拦截事件,ListView判断是自己的手势就做出请求父级不要拦截我的请求->正常分发,否则就请求拦截,此时是否拦截受到父级的onInterceptTouchEven方法t影响

    重写ListView的事件分发dispatchTouchEvent方法,判断水平方向滑动时请求父级拦截我,其它情况请求不要拦截我。

    
    public class MyListView extends ListView {
    
        public MyListView(Context context) {
            super(context);
        }
    
        public MyListView(Context context, AttributeSet attrs) {
            super(context, attrs);
        }
    
    
        private int mLastX;
        private int mLastY;
    
        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            int x = (int) ev.getX();
            int y = (int) ev.getY();
    
            final int action = ev.getAction() & MotionEvent.ACTION_MASK;
            switch (action) {
                case MotionEvent.ACTION_DOWN:
                    getParent().requestDisallowInterceptTouchEvent(true);
                    break;
                case MotionEvent.ACTION_MOVE:
                    //水平移动的增量
                    int deltaX = x - mLastX;
                    //竖直移动的增量
                    int deltaY = y - mLastY;
                    //当水平增量大于竖直增量时,表示水平滑动,此时需要父View去处理事件
                    if (Math.abs(deltaX) > Math.abs(deltaY)){
                        getParent().requestDisallowInterceptTouchEvent(false);
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    break;
                default:
                    break;
            }
            mLastX = x;
            mLastY = y;
            return super.dispatchTouchEvent(ev);
        }
    
    
    }

     

    重写ViewPager的事件拦截方法,让其默认拦截

    
    public class BadViewPager extends ViewPager {
    
        public BadViewPager(@NonNull Context context) {
            super(context);
        }
    
        public BadViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
        }
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
    
            return true;
    
        }
    }

    测试发现这样ListView并不能正常滑动,被拦截了。问题出在哪里?

    我们先把正确的代码写出来,修改ViewPager的拦截事件方法在down的时候返回false不拦截,其它返回true。这样就ViewPager和ListView就能正常滑动了

     @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            final int action = ev.getAction() & MotionEvent.ACTION_MASK;
            if (action == MotionEvent.ACTION_DOWN) {
                super.onInterceptTouchEvent(ev);
                return false;
            }
            return true;
    
        }

    通过代码发现外部拦截法相比内部拦截法要简单很多也容易理解。解决事件冲突,总结来说就是这两种方式,实际掌握事件分发机制后,相信大家能够灵活运用

    内部拦截父ViewGroup的onInterceptTouchEvent拦截方法,为何要这么写?

    四 ViewPager嵌套ListView的滑动冲突,内部拦截法为何ViewPager的onInterceptTouchEvent要做判断而不是直接返回true?

     

    我们重温下Android事件分发流程源码解析一中ViewGroup事件分发方法的源码。注意这个ViewGroup对应到我们例子的ViewPager

    class:ViewGroup:
    
       @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            
    		......//省略
            boolean handled = false;
            if (onFilterTouchEventForSecurity(ev)) {     //判断屏幕是否隐藏等
    				
                final int action = ev.getAction();
                final int actionMasked = action & MotionEvent.ACTION_MASK;
    
                // Handle an initial down.
                //该方法在事件冲突的内部拦截法当中有重要作用,这里暂不解析
                if (actionMasked == MotionEvent.ACTION_DOWN) {
                    cancelAndClearTouchTargets(ev);
                    resetTouchState();
                }
    			//**重点 intercepted就是判断该ViewGroup是否需要直接处理事件的标记
                // Check for interception.
                final boolean intercepted;
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || mFirstTouchTarget != null) {
                    final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                
                    if (!disallowIntercept) {
    //    用内部拦截法时,down事件的时候,我们在子类的requestDisallowInterceptTouchEvent传入了true,就是希望父类不要拦截我,从而不进入这个判断。
    //但是坑的是,在down的时候,在上面的if判断中对disallowIntercept进行了重置为false,不受子类requestDisallowInterceptTouchEvent的影响了,也就是说肯定会进入这个判断
                        intercepted = onInterceptTouchEvent(ev);
                        ev.setAction(action); // restore action in case it was changed
                    } else {
                         
                        intercepted = false;
                    }
                } else {
                    // There are no touch targets and this action is not an initial down
                    // so this view group continues to intercept touches.
                    intercepted = true;
                }
    }
    

     

    ViewGroup的事件拦截方法第一步就是给intercpted赋值,当intercepted为true时,就不会通过for循环去找子View分发事件。我们看看在上述源码里面发现在ACTION_DOWN的时候,对disallowIntercept进行了赋值->true,不受子类方法requestDisallowInterceptTouchEvent(请求父类不要拦截我)的影响。

    那么本来我们希望的是在action_down的时候希望父类不要拦截我,不进入if (!disallowIntercept) {}这个判断从而让intercepted为false。现在进入if (!disallowIntercept) {}了这个判断-> intercepted = onInterceptTouchEvent(ev),所以我们正确的方法是应该在父类ViewPager的onInterceptTouchEvent加一个判断,DOWN时返回false,其它true。

     

    4.1 ACTION_CANCEL什么时候调用?

    我们在事件分发Android事件分发流程源码解析一当中讲述了DOWN、MOVE、UP、CANCEL四个方法的调用时机。其中CANCEL在被上层事件拦截的时候调用,看看源码分析其原因

    ViewGroup:
     
     @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {  
    
    //...省略
     // Dispatch to touch targets.
                if (mFirstTouchTarget == null) {
                    // No touch targets so treat this as an ordinary view.
                    handled = dispatchTransformedTouchEvent(ev, canceled, null,
                            TouchTarget.ALL_POINTER_IDS);
                } else {
                    // Dispatch to touch targets, excluding the new touch target if we already
                    // dispatched to it.  Cancel touch targets if necessary.
                    TouchTarget predecessor = null;
                    TouchTarget target = mFirstTouchTarget;
                    while (target != null) {
                        final TouchTarget next = target.next;
                        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                            handled = true;
                        } else {
                            
    //    move时进入这里
                            final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                    || intercepted;
                            if (dispatchTransformedTouchEvent(ev, cancelChild,
                                    target.child, target.pointerIdBits)) {
                                handled = true;
                            }
                            if (cancelChild) {
                                if (predecessor == null) {
                                    mFirstTouchTarget = next;
                                } else {
                                    predecessor.next = next;
                                }
                                target.recycle();
                                target = next;
                                continue;
                            }
                        }
                        predecessor = target;
                        target = next;
                    }
                }
    }
    //...省略

    以上是ViewGroup事件分发方法的部分源码,我们在Android事件分发流程源码解析一,总结了手指从第一次触摸DOWN到滑动MOVE时如何命中目标。此刻的场景是命中目标后手指在屏幕上面MOVE,mFirstTouchTarget此时不为空。会调用这个 if (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) {}判断,并且其中的cancelChild此时为true(因为我们的ListView在MOVE时,根据滑动手势左右滑动时要求父类拦截我->intercepted为true)。那么我们来看看dispatchTransformedTouchEvent方法cancelChild传true时候的源码:

     private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                View child, int desiredPointerIdBits) {
            final boolean handled;
    
            // Canceling motions is a special case.  We don't need to perform any transformations
            // or filtering.  The important part is the action, not the contents.
            final int oldAction = event.getAction();
            if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
                //调用ACTION_CANCEL方法
                event.setAction(MotionEvent.ACTION_CANCEL);
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    handled = child.dispatchTouchEvent(event);
                }
                event.setAction(oldAction);
                return handled;
            }
    //。。省略
    }

    很容易看出,在cancel传入为true的时候,进入判断调用ACTION_CANCEL方法。这也就是为何在ACTION_CANCEL方法在被上级拦截时调用的原因了。

    4.2 ListView被上层拦截了怎么将事件交还给ViewPager

    我们接着刚刚的分析步骤dispatchTransformedTouchEvent方法会进入handled = child.dispatchTouchEvent(event),其child为ListView将事件是否消费交给了ListView处理。(记住此时条件在用户down并且move后触发了ViewPager的拦截方法导致intercepted为true,此时命中了消费事件的目标ListView,即mFirstTouchTarget不为空)我们接着ViewGroup的事件方法走,此时走到了 if (cancelChild) {}我们注意进入该判断后会调用 mFirstTouchTarget = next方法,会将mFirstTouchTarget置为空。

    此时ViewGroup的dispatchTouchEvent事件分发方法走完了一次,随着手指的滑动再次进入dispatchTouchEvent(一定要记住MOVE事件会多次调用),很容易看出最终会进入

    调用dispatchTransformedTouchEvent方法,并且child传null。我们通过查看dispatchTransformedTouchEvent的源码,得知当child为null时,会调用其父级的dispatchTouchEvent方法。这样就将事件交回了ViewPager

     

     

    展开全文
    LJ_GOD 2021-01-11 14:40:18
  • 5星
    1.7MB wenbai317 2014-11-11 09:29:23
  • 5星
    1.47MB sinyu890807 2013-08-21 21:13:51
  • 10KB ftmtshuashua 2015-08-25 14:59:46
  • 66.3MB weixin_39840588 2019-08-13 04:56:01
  • #定制Android滑动关闭Activity 现在手机屏幕越来越大,而页面的退出按键通常设置在屏幕左上角,这就导致了当单手操作时用户体验及其不好。虽然也能通过实体按键返回和现在流行的全面屏手势解决,但是感觉会很...

    定制Android滑动关闭Activity

    现在手机屏幕越来越大,而页面的退出按键通常设置在屏幕左上角,这就导致了当单手操作时用户体验及其不好。虽然也能通过实体按键返回和现在流行的全面屏手势解决,但是感觉会很生硬,这里就定制一个用户体验极佳的滑动关闭功能。

    写在前面

    相信大多数人日常刷各种爱啪啪的时候都有使用过滑动关闭,我也是因为用了觉得很舒服才决定写这篇文章。

    对于这个功能实际上已经有一些封装好的三方库可以让我们直接使用了。那为什么我还要费力不讨好的自己来写一遍呢,第一:为了加深对Android中Window和View的理解。第二:目前封装好的库存在一些瑕疵,不能很好的满足我的要求。
    展示

    思路

    熟悉Activity生命周期的童鞋应该知道多个Activity是以压栈的形式进行管理的,并且通过查看官方的描述可以大概猜测出当存在多个Activity时只是将新的覆盖在了之前的上面使其不可见。因此要实现滑动关闭效果只需要通过手势将顶部Activity移开,从而让底部被遮挡的显示出来。

    步骤

    Window和ContentView

    我们一般会使用Activity的setContentView来设置显示的布局,但是为什么这样做就涉及到Android显示机制了。

    简单来说每个Activity都是一个WindowWindow会绑定一个根布局DecorView

    DecorView中包含一个垂直方向的LinearLayout

    LinearLayout里面为TitleBarContentView

    是不是很眼熟呢,没错,平时我们调用的setContentView就是设置的这个ContentView,该View继承自FrameLayout,我们设置的布局就是放在这个里面的。知道了这些才能进行后续的动画操作。

    Theme

    Window默认是有背景色的并且不能含有透明通道,因此即便我们将Window下整个布局移开依然看不到底部的Activity。这里就必须在Theme中添加一个参数。

            <item name="android:windowIsTranslucent">true</item>

    理论上这样就可以了,但是为了更好的效果,我们需要对背景设置一个渐变的半透明色,它会随着拉动的距离而变淡,于是会将背景设置给DecorView,这样的话位移动画只能设置给DecorView子控件,这里就会发现一些问题,那就是statusBar不会跟着位移,对此我也借鉴了一些已存在并被广泛采用的三方库发现确实存在这个问题。因此我想到了一个曲线救国的解决方案,那就是去掉statusBar。具体参数如下:

            <item name="android:windowTranslucentStatus">false</item>
            <item name="android:windowTranslucentNavigation">true</item>
            <!--Android 5.x开始需要把颜色设置透明,否则导航栏会呈现系统默认的浅灰色-->
            <item name="android:statusBarColor">@color/colorStatBar</item>

    Activity

    新建一个SwipeBackActivity,重写onCreate方法,设置Window为透明。

            window.setBackgroundDrawableResource(R.color.colorTransparent)

    接着重写setContentView,拿到Activity设置的具体View,上面提到了为了效果我们去掉了statusBar,这样整个布局就会顶在屏幕最上面不是很美观,因此我们就需要自己添加一个状态栏,但是总不能每个layout都去手动添加这样太麻烦了,就找到了官方为我们提供的一个参数fitsSystemWindows,设置之后系统会自动帮我们填充一个状态栏高度的控件,并且是沉浸式的。

            findViewById<ViewGroup>(android.R.id.content).let {
                    it.getChildAt(0).apply {
                    fitsSystemWindows = true
                }
            }

    TouchEvent

    既然是滑动操作肯定就需要监听屏幕点击事件,我这里是选择监听了dispatchTouchEvent事件,自己来判断手势操作,也可以使用一些封装好的工具类例如:GestureDetector等。

    对点击事件不清楚的可以看我的另一篇文章「Android触摸事件」,具体实现逻辑就不详细叙述了,大致思路就是在滑动时判断当前是否为关闭Activity的操作,如果是就消费滑动事件,并且对view设置位移动画。具体代码:

        override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
            when (ev.action) {
                MotionEvent.ACTION_DOWN -> {
                    isFirst = true
                    swipeType = false
                    lastPoint.set(ev.x, ev.y)
                }
                MotionEvent.ACTION_MOVE -> {
                    val changeX = ev.x - lastPoint.x
                    val changeY = ev.y - lastPoint.y
                    if (isFirst && Math.abs(changeX) > touchSlop && Math.abs(changeY) > Math.abs(changeX) * 1.5) {
                        isFirst = false
                    }
                    if (isFirst && Math.abs(changeX) > touchSlop && Math.abs(changeX) > Math.abs(changeY) * 1.5) {
                        swipeType = true
                    }
                    if (swipeType) {
                        if (tranX + changeX < -shadowDp) {
                            return true
                        }
                        tranX += changeX
                        val a = shadowMax - tranX * shadowPer
                        val shadow = a.toInt().toString(16)
                        decorView.setBackgroundColor(Color.parseColor("#${shadow}000000"))
                        transView.translationX = tranX
                        lastPoint.set(ev.x, ev.y)
                        return true
                    }
                }
                MotionEvent.ACTION_UP -> {
                    if (swipeType) {
                        if (tranX >= windowSize.x / 3) {
                            startAnim(true)
                        } else {
                            startAnim(false)
                        }
                        return true
                    }
                }
            }
            return super.dispatchTouchEvent(ev)
        }

    SmoothScroll

    从上面代码可以看出,我是根据当前滑动距离来判断是否需要关闭Activity,如果滑动距离超过屏幕大小的1/3就关闭,没有的话就恢复原来位置。

    当松手后,如果直接执行关闭或者复原操作会感觉很生硬,所以我添加上一个短时间的平滑过度动画,是ObjectAnimator的一种常规应用,具体代码如下:

        private fun startAnim(isExit: Boolean) {
            ObjectAnimator().apply {
                duration = 300
                if (isExit) {
                    setFloatValues(tranX, windowSize.x.toFloat() - shadowDp)
                    addListener(object : Animator.AnimatorListener {
                        override fun onAnimationRepeat(animation: Animator?) {
                        }
    
                        override fun onAnimationEnd(animation: Animator?) {
                            finish()
                        }
    
                        override fun onAnimationCancel(animation: Animator?) {
                        }
    
                        override fun onAnimationStart(animation: Animator?) {
                        }
    
                    })
                } else {
                    setFloatValues(tranX, -shadowDp)
                    addListener(object : Animator.AnimatorListener {
                        override fun onAnimationRepeat(animation: Animator?) {
                        }
    
                        override fun onAnimationEnd(animation: Animator?) {
                            tranX = -shadowDp
                        }
    
                        override fun onAnimationCancel(animation: Animator?) {
                        }
    
                        override fun onAnimationStart(animation: Animator?) {
                        }
    
                    })
                }
                interpolator = DecelerateInterpolator()
                addUpdateListener { animation ->
                    tranX = animation.animatedValue as Float
                    val a = shadowMax - tranX * shadowPer
                    if (a >= 16) {
                        val shadow = a.toInt().toString(16)
                        decorView.setBackgroundColor(Color.parseColor("#${shadow}000000"))
                    }
                    transView.translationX = tranX
                }
                start()
            }
        }

    半透明遮罩和阴影

    除了上述说到的需要对背景设置一个半透明遮罩之外,为了更好的效果,还需要对拖拽的部分添加上一些阴影来增加层次感。这些阴影是需要加在显示部分之外,而我们知道View大小是不可能超过其父布局显示内容的。因此就需要添加新的布局来显示阴影。

    逻辑比较复杂和繁琐,直接上代码:

        override fun setContentView(layoutResID: Int) {
            super.setContentView(layoutResID)
            findViewById<ViewGroup>(android.R.id.content).let {
                //viewGroup,将背景和content绑定在一起
                val viewGroup = FrameLayout(this).apply {
                    layoutParams = ViewGroup.LayoutParams(windowSize.x + shadowDp.toInt(), ViewGroup.LayoutParams.MATCH_PARENT)
                    translationX = tranX
                }
                //背景View
                View(this).apply {
                    layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
                    setBackgroundResource(R.drawable.edge_shadow)
                    viewGroup.addView(this)
                }
                //contentView
                it.getChildAt(0).apply {
                    val params = layoutParams
                    params.width = windowSize.x
                    layoutParams = params
                    fitsSystemWindows = true
                    translationX = shadowDp
                    it.removeView(this)
                    viewGroup.addView(this)
                }
                it.addView(viewGroup)
                transView = viewGroup
            }
        }

    edge_shadow.xml:

    <?xml version="1.0" encoding="utf-8"?>
    <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
        <item
            android:start="8dp">
            <shape>
                <solid android:color="@color/colorBackground"/>
            </shape>
        </item>
        <item
            android:width="8dp">
            <shape android:shape="rectangle">
                <!--颜色渐变范围-->
                <gradient
                    android:endColor="#3f000000"
                    android:startColor="#00000000"/>
            </shape>
        </item>
    
    </layer-list>
    

    其中的坑

    正常这样写是没有问题的,但是就面临着需要在Manifest中挨个添加android:theme="@style/TranslucentTheme"。这无形中便增加了我们的工作量和代码的耦合度。肯定有人会说这还不简单,在代码里面动态设置Theme不就好了吗,我最开始也是这么想的,果然实践是检验问题的唯一真理,设置了之后最最重要的属性windowIsTranslucent没有生效,然后我以为是设置主题的时间没掌握好接下来试了各种方法,不出意外的全部失效

    最后总结出来,在Activity生命周期内无论在任何地方设置Theme其中windowIsTranslucent=true参数都会失效。那么难道就没办法了吗?皇天不负有心人,在我扒Window以及Activity原码之后终于让我发现了一个Activity的私有方法:convertToTranslucent(),话不多说直接上代码,利用反射暴力更改。

        /**
         * 反射设置windowIsTranslucent
         */
        private fun convertActivityToTranslucentAfterL(activity: Activity) {
            try {
                val getActivityOptions = Activity::class.java.getDeclaredMethod("getActivityOptions")
                getActivityOptions.isAccessible = true
                val options = getActivityOptions.invoke(activity)
                val classes = Activity::class.java.declaredClasses
                var translucentConversionListenerClazz: Class<*>? = null
                for (clazz in classes) {
                    if (clazz.simpleName.contains("TranslucentConversionListener")) {
                        translucentConversionListenerClazz = clazz
                    }
                }
                val convertToTranslucent = Activity::class.java.getDeclaredMethod("convertToTranslucent",
                        translucentConversionListenerClazz, ActivityOptions::class.java)
                convertToTranslucent.isAccessible = true
                convertToTranslucent.invoke(activity, null, options)
            } catch (t: Throwable) {
            }
        }

    听说在5.0之前该方法参数不同,现在5.0之前的机型也几乎可以忽略不计了,如果有需求的自己去看看。

    就当我以为万事大吉的时候,叕叕叕他娘的出bug了!!转场动画共享元素动画全部失效。
    按理说Translucent代码中实现和在Theme中定义效果是完全一样的,然而。。。又是漫长的尝试和思考,最后终于找到一个解决方法:在Activity启动之后调用convertToTranslucent(),本来是准备放在onResume中调用但是不知道为什么没效果,最后无奈只能放在屏幕点击事件中。
    打完收工。

    写在最后

    以上就完成了一个自定义的滑动关闭手势动画,我目前使用起来也没有遇到什么问题,识别率、滑动冲突、误触以及动画方面都还可以。后续可以添加根据用户滑动速度的来判断是否关闭的机制。当页面存在垂直列表时也不会有问题,至于横向的话暂时没有试过。

    如果有什么问题欢迎留言,如果对横向冲突有更好解决方案的也感谢指出。

    展开全文
    ccw0054 2018-06-15 13:01:20
  • nicolelili1 2018-03-04 19:28:51
  • 5星
    5.34MB tetsuyakun 2014-06-19 12:27:59
  • 157KB weixin_38563871 2021-01-05 23:56:48
  • 57KB weixin_38518376 2020-08-28 20:05:06
  • weixin_34038652 2019-03-26 14:27:00
  • 443KB wg779115475 2017-07-15 21:39:11
  • a_sid 2019-06-25 00:26:18
  • 783KB weixin_39840387 2019-07-10 19:34:54
  • 5星
    936KB sunboy_2050 2012-04-22 22:49:58
  • 63KB weixin_38620839 2020-09-02 15:11:43

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 155,729
精华内容 62,291
关键字:

android滑动

友情链接: prim.rar