美文网首页
一个能随手势滑动消除的 Android 应用内悬浮全局通知的实现

一个能随手势滑动消除的 Android 应用内悬浮全局通知的实现

作者: 柴柴土 | 来源:发表于2018-03-04 18:34 被阅读550次

    1. 例子

    先来看两个例子。

    1.1 小米手机上收到通知时

    小米悬浮通知.gif

    1.2 锤子手机上复制链接时

    锤子悬浮通知.gif

    都是在屏幕上方显示了一个悬浮的通知,并且这个通知是在切换 activity 的时候或者按返回键的时候不消失的,当你横向滑动的时候才消失。

    下面来实现类似的效果。

    本文最后实现的效果如下:

    悬浮通知.gif

    2. 实现

    由于悬浮通知不受到 activity 的控制,因此需要使用悬浮窗来实现。但是先不管悬浮窗,先实现一个可以随手滑动的消失的 view。

    这个效果要满足以下几个要求:

    1. 当手指在屏幕上滑动不松开的时候,悬浮通知也随着手指滑动。
    2. 当手指松开的时候,判断滑动方向,来决定是向右还是向左消失。
    3. 当快速滑动(fling)的时候,朝着对应的方向消失。
    4. 滑动的时候,透明度(alpha)也相应地跟着变化。

    可以重写 onTouch 方法,但是那样比较麻烦。这里使用一个 GestureDetector 类,可以方便的判断手势。

    GestureDetector 的构造方法之一是:

    public GestureDetector(Context context, OnGestureListener listener)
    

    传入两个参数,第二个参数是一个手势监听回调;在回调中就可以获取各种位置信息,来进行处理。

    创建 GestureDetector 的代码如下:

    mDetector = new GestureDetector(context, new GestureDetector.OnGestureListener() {
                @Override
                public boolean onDown(MotionEvent e) {
                    return false;
                }
    
                @Override
                public void onShowPress(MotionEvent e) {
    
                }
    
                @Override
                public boolean onSingleTapUp(MotionEvent e) {
                    return true;
                }
    
                @Override
                public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                    float x1 = e1.getX();
                    float x2 = e2.getX();
                    int offsetX = (int) (x2 - x1);
                    layoutWithAlpha(getLeft() + offsetX, getTop(), getRight() + offsetX, getBottom());
                    return false;
                }
    
                @Override
                public void onLongPress(MotionEvent e) {
    
                }
    
                @Override
                public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                    Log.d(TAG, "onFling: =" + (e2.getX() - e1.getX()) + " velocityX=" + velocityX);
                    float fling = e2.getX() - e1.getX();
                    if (fling > 0) {
                       disappear(DIRECTION_LEFT);
                    } else {
                       disappear(DIRECTION_RIGHT);
                    }
                    return false;
                }
            });
    

    其中,在 onScroll 方法中,计算出要滑动的位置:

    float x1 = e1.getX();
    float x2 = e2.getX();
    int offsetX = (int) (x2 - x1);
    

    e1 代表的是手指最初按下去的时候的 MotionEvent,e2 代表的是滑动到执行该 onScroll 的时候手指的位置,e1.getX() - e2.getX() 即代表在 x 方向上滑动了多少。

    接下来通过 layoutWithAlpha 来重新摆放该 view 的位置并设置 alpha 值,传入 4 个参数分别是左上右下的值。

    private void layoutWithAlpha(int l, int t, int r, int b) {
        layout(l, t, r, b);
        float alpha = (screenWidth - Math.abs(getLeft())) / screenWidth;
        setAlpha(alpha);
    }
    

    layoutWithAlpha这个方法其实就是调用 View 内部的 layout 来重新摆放位置,然后计算出相应的 alpha 值。

    再看一下 onFling 方法。onFling 方法中的 e1 和 e2 与 onScroll 方法中的是一样的。onFling先通过 e2.getX() - e1.getX() 判断滑动的方向,大于 0 表示向左滑动,反之向右。

    这里 disappear 方法代码如下:

    private void disappear(int direction) {
        final int speed = direction == DIRECTION_LEFT ? 66 : -66;
        Runnable r = new Runnable() {
            @Override
            public void run() {
                int left = getLeft();
                int right = getRight();
                layoutWithAlpha(left + speed, getTop(), right + speed, getBottom());
                if (left < screenWidth && right > 0) {
                    post(this);
                  }
                }
            }
        };
        post(r);
    }
    

    这里 66 是我随便设定的值,表示一次性移动多少 px。 disappear 方法就是不断的调用 layout 来进行移动,此时,SwipeNotification 的所有代码如下,但是此时 SwipeNotification 还只能放在 activity 内:

    public class SwipeNotification extends LinearLayout {
    
        private static final String TAG = "SwipeNotification";
    
        private static final int DIRECTION_LEFT = 1;
    
        private static final int DIRECTION_RIGHT = 2;
    
        private static final int SPEED_PX = 66;
    
        private GestureDetector mDetector;
    
        private boolean isDisappearing = false;
    
        private ImageView mImageView;
    
        private TextView mTextView;
    
        private static float screenWidth;
    
        public SwipeNotification(@NonNull Context context) {
            this(context, null);
        }
    
        public SwipeNotification(@NonNull Context context, @Nullable AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public SwipeNotification(@NonNull Context context, @Nullable AttributeSet attrs,
                                  int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init(context);
        }
    
        private void init(Context context) {
            screenWidth = getScreenWidth(context);
            View inflate = LayoutInflater.from(context).inflate(R.layout.item_notification, this);
            mImageView = inflate.findViewById(R.id.image_view);
            mTextView = inflate.findViewById(R.id.text_view);
            mDetector = new GestureDetector(context, new GestureDetector.OnGestureListener() {
                @Override
                public boolean onDown(MotionEvent e) {
                    float x = e.getX();
                    float y = e.getY();
                    return false;
                }
    
                @Override
                public void onShowPress(MotionEvent e) {
    
                }
    
                @Override
                public boolean onSingleTapUp(MotionEvent e) {
                    Log.d(TAG, "onSingleTapUp: ");
                    return true;
                }
    
                @Override
                public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                    float x1 = e1.getX();
                    float x2 = e2.getX();
                    int offsetX = (int) (x2 - x1);
                    layoutWithAlpha(getLeft() + offsetX, getTop(), getRight() + offsetX, getBottom());
                    return false;
                }
    
                @Override
                public void onLongPress(MotionEvent e) {
    
                }
    
                @Override
                public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                    Log.d(TAG, "onFling: =" + (e2.getX() - e1.getX()) + " velocityX=" + velocityX);
                    float fling = e2.getX() - e1.getX();
                    if (fling > 0) {
                        disappear(DIRECTION_LEFT);
                    } else {
                        disappear(DIRECTION_RIGHT);
                    }
                    return false;
                }
            });
        }
    
        public void setText(CharSequence text) {
            mTextView.setText(text);
        }
    
        public void setImageResource(int resId) {
            mImageView.setImageResource(resId);
        }
    
        private void layoutWithAlpha(int l, int t, int r, int b) {
            layout(l, t, r, b);
            float alpha = (screenWidth - Math.abs(getLeft())) / screenWidth;
            Log.d(TAG, "layoutWithAlpha: alpha = " + alpha);
            setAlpha(alpha);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            boolean detectedUp = event.getAction() == MotionEvent.ACTION_UP;
            if (!mDetector.onTouchEvent(event) && detectedUp) {
                onUp(event);
    //            return super.onTouchEvent(event);
            }
            return true;
        }
    
        private void onUp(MotionEvent event) {
            Log.d(TAG, "onUp: ");
            if (isDisappearing) {
                return;
            }
            autoDisappear();
        }
    
        private void autoDisappear() {
            Log.d(TAG, "autoDisappear: ");
            if (getLeft() > screenWidth / 5) {
                disappear(DIRECTION_LEFT);
            } else if (getRight() < screenWidth * 4 / 5) {
                disappear(DIRECTION_RIGHT);
            }
        }
    
        private void disappear(int direction) {
            isDisappearing = true;
            final int speed = direction == DIRECTION_LEFT ? SPEED_PX : -SPEED_PX;
            Log.d(TAG, "disappear: speed=" + speed);
            Runnable r = new Runnable() {
                @Override
                public void run() {
                    int left = getLeft();
                    int right = getRight();
                    layoutWithAlpha(left + speed, getTop(), right + speed, getBottom());
                    Log.d(TAG, "run: layout");
                    if (left < screenWidth && right > 0) {
                        post(this);
                    }
                }
            };
            post(r);
        }
    
        public static int getScreenWidth(Context context) {
            WindowManager wm = (WindowManager)
                    context.getSystemService(Context.WINDOW_SERVICE);
            return wm.getDefaultDisplay().getWidth();
        }
    }
    

    需要注意的一点是,GestureDetector 没有判断手指抬起的时候的回调,因此需要手动在
    onTouchEvent 当中捕获到这个手指抬起的动作,然后自动滑动消失。(注意:onSingleTapUp 只是单击抬起才触发,当滑动后不会再触发这个回调)

    @Override
        public boolean onTouchEvent(MotionEvent event) {
            boolean detectedUp = event.getAction() == MotionEvent.ACTION_UP;
            if (!mDetector.onTouchEvent(event) && detectedUp) {
                onUp(event);
            }
            return true;
        }
    

    这个悬浮通知的 xml 布局如下:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@drawable/shadow"
        android:orientation="horizontal">
    
        <ImageView
            android:id="@+id/image_view"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:contentDescription="ic_launcher"
            android:src="@mipmap/ic_launcher" />
    
        <TextView
            android:id="@+id/text_view"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:gravity="center"
            android:text="test" />
    
    </LinearLayout>
    

    然后再在 activity 的布局中引用一下:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    
        <com.enhao.rxjavatest.SwipeNotification
            android:layout_width="match_parent"
            android:layout_height="50dp" />
            
        <com.enhao.rxjavatest.SwipeNotification
            android:layout_width="match_parent"
            android:layout_height="50dp" />
    
    </LinearLayout>
    

    ok,基本就实现了类似的效果,如下图:

    悬浮通知1.gif

    但是这个是放在 activity 中的。为了实现在应用的全局内都能显示通知,就是说切换 activity,通知不消失,就要使用悬浮框了。

    悬浮窗的权限申请不在本文的范围内,而且众多国产 rom 的对悬浮窗的权限管理不太一样。适配起来需要费一定的劲。

    本文假设已经获取到了悬浮窗的权限。

    由于这个悬浮窗是应用内全局的,因此打算把它做成一个单例类。

    代码如下:

    public class SwipeNotificationManager {
    
        private static final String TAG = "FloatingWindowManager";
    
        private static SwipeNotificationManager sManager;
    
        private WindowManager mWindowManager;
        private WindowManager.LayoutParams mParams;
    
        // 用一个 LinearLayout 作为包含所有通知的容器
        private LinearLayout mNotificationContainer;
    
        private Context mContext;
    
        // 用来记录包含通知的 LinearLayout 是否已经添加到窗口里了
        private boolean isAdded;
    
        // 最大允许显示的通知条目数量
        private int mMaxCount = 3;
    
        private SwipeNotificationManager(Context context) {
            mContext = context;
            mParams = new WindowManager.LayoutParams();
    
            // 获取 WindowManager
            mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    
            // 新建一个包含所有通知的容器
            mNotificationContainer = new LinearLayout(context);
            mNotificationContainer.setOrientation(LinearLayout.VERTICAL);
    
            // 设定一下 WindowManager.LayoutParams 的一些参数值
            initWindowParams(mWindowManager, mParams);
    
            // 将 mNotificationContainer 添加到窗口上
            mWindowManager.addView(mNotificationContainer, mParams);
            isAdded = true;
        }
    
        public static SwipeNotificationManager getInstance(Context context) {
            if (sManager == null) {
                synchronized (SwipeNotificationManager.class) {
                    if (sManager == null) {
                        sManager = new SwipeNotificationManager(context.getApplicationContext());
                    }
                }
            }
            return sManager;
        }
    
        public void addNotification(final CharSequence text, int imageId) {
            // 新建一条通知
            final SwipeNotification notification = new SwipeNotification(mContext);
            // 设置通知的文本和图片
            notification.setText(text);
            notification.setImageResource(imageId);
            // 给通知添加点击事件
            notification.setOnClickNotificationListener(
                    new SwipeNotification.OnClickNotificationListener() {
                        @Override
                        public void onClickNotification() {
                            Toast.makeText(mContext, text, Toast.LENGTH_SHORT).show();
                        }
                    });
            // 若包含通知的 LinearLayout 已经被移除窗口,就加上去
            if (!isAdded) {
                mWindowManager.addView(mNotificationContainer, mParams);
                Log.d(TAG, "addNotification: addView");
                isAdded = true;
            }
            // 获取一共有多少条通知
            int childCount = mNotificationContainer.getChildCount();
    
            // 已经显示的通知数目大于了最大值,就将第一条移除
            if (childCount == mMaxCount) {
                mNotificationContainer.removeViewAt(0);
            }
    
            // 将通知添加进 mNotificationContainer
            mNotificationContainer.addView(notification);
    
            // 通知已经通过滑动移除出屏幕的时候,将通知从 mNotificationContainer 中移除
            notification.setOnDisappearListener(new SwipeNotification.OnDisappearListener() {
                @Override
                public void onDisappear() {
                    mNotificationContainer.removeView(notification);
                    // 如果 mNotificationContainer 里面已经没有通知了,将 mNotificationContainer 从窗口移除
                    if (mNotificationContainer.getChildCount() == 0) {
                        mWindowManager.removeViewImmediate(mNotificationContainer);
                        isAdded = false;
                    }
                }
            });
    
        }
    
        private void initWindowParams(WindowManager manager, WindowManager.LayoutParams params) {
            // android 8.0 以上,要将 type 设为 TYPE_APPLICATION_OVERLAY
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
            } else {
                params.type = WindowManager.LayoutParams.TYPE_PHONE;
            }
            mParams.format = PixelFormat.RGBA_8888;
            params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
            //调整悬浮窗显示的停靠位置为置顶
            params.gravity = TOP;
            //以屏幕左上角为原点,设置x、y初始值
            params.x = 0;
            params.y = 0;
            //设置悬浮窗口长宽
            params.width = MATCH_PARENT;
            params.height = WRAP_CONTENT;
        }
    
        /**
         * 设置一下最大的显示通知的数量
         */
        private void setMaxCount(int maxCount) {
            mMaxCount = maxCount;
        }
    

    其中,由于 GestureDetector 消耗掉了触摸事件,因此不能给 SwipeNotification 直接 setOnClickListener;为了使点击事件能够处理,需要在 GestureDetector 的 onSingleTapUp 里面添加一个回调。修改后的 SwipeNotification 代码如下:

    public class SwipeNotification extends LinearLayout {
    
        private static final String TAG = "SwipeNotification";
    
        private static final int AUTO_DISMISS_TIME = 2000;
    
        private static final int DIRECTION_LEFT = 1;
    
        private static final int DIRECTION_RIGHT = 2;
    
        private static final int SPEED_PX = 66;
    
        private GestureDetector mDetector;
    
        private OnDisappearListener mDisappearListener;
    
        private OnClickNotificationListener mClickNotificationListener;
    
        private boolean isDisappearing = false;
    
        private ImageView mImageView;
    
        private TextView mTextView;
    
        private static float screenWidth;
    
        public SwipeNotification(@NonNull Context context) {
            this(context, null);
        }
    
        public SwipeNotification(@NonNull Context context, @Nullable AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public SwipeNotification(@NonNull Context context, @Nullable AttributeSet attrs,
                                 int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            init(context);
        }
    
        private void init(Context context) {
            screenWidth = getScreenWidth(context);
            View inflate = LayoutInflater.from(context).inflate(R.layout.item_notification, this);
            mImageView = inflate.findViewById(R.id.image_view);
            mTextView = inflate.findViewById(R.id.text_view);
            mDetector = new GestureDetector(context, new GestureDetector.OnGestureListener() {
                @Override
                public boolean onDown(MotionEvent e) {
                    float x = e.getX();
                    float y = e.getY();
                    return false;
                }
    
                @Override
                public void onShowPress(MotionEvent e) {
    
                }
    
                @Override
                public boolean onSingleTapUp(MotionEvent e) {
                    Log.d(TAG, "onSingleTapUp: ");
                    if (mClickNotificationListener != null) {
                        mClickNotificationListener.onClickNotification();
                    }
                    return true;
                }
    
                @Override
                public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                    float x1 = e1.getX();
                    float x2 = e2.getX();
                    int offsetX = (int) (x2 - x1);
                    layoutWithAlpha(getLeft() + offsetX, getTop(), getRight() + offsetX, getBottom());
                    return false;
                }
    
                @Override
                public void onLongPress(MotionEvent e) {
    
                }
    
                @Override
                public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                    Log.d(TAG, "onFling: =" + (e2.getX() - e1.getX()) + " velocityX=" + velocityX);
                    float fling = e2.getX() - e1.getX();
                    if (fling > 0) {
                        disappear(DIRECTION_LEFT);
                    } else {
                        disappear(DIRECTION_RIGHT);
                    }
                    return false;
                }
            });
        }
    
        public void setText(CharSequence text) {
            mTextView.setText(text);
        }
    
        public void setImageResource(int resId) {
            mImageView.setImageResource(resId);
        }
    
        private void layoutWithAlpha(int l, int t, int r, int b) {
            layout(l, t, r, b);
            float alpha = (1080f - Math.abs(getLeft())) / 1080f;
            Log.d(TAG, "layoutWithAlpha: alpha = " + alpha);
            setAlpha(alpha);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            boolean detectedUp = event.getAction() == MotionEvent.ACTION_UP;
            if (!mDetector.onTouchEvent(event) && detectedUp) {
                onUp(event);
    //            return super.onTouchEvent(event);
            }
            return true;
        }
    
        private void onUp(MotionEvent event) {
            Log.d(TAG, "onUp: ");
            if (isDisappearing) {
                return;
            }
            autoDisappear();
        }
    
        private void autoDisappear() {
            Log.d(TAG, "autoDisappear: ");
            if (getLeft() > screenWidth / 5) {
                disappear(DIRECTION_LEFT);
            } else if (getRight() < screenWidth * 4 / 5) {
                disappear(DIRECTION_RIGHT);
            }
        }
    
        private void disappear(int direction) {
            isDisappearing = true;
            final int speed = direction == DIRECTION_LEFT ? SPEED_PX : -SPEED_PX;
            Log.d(TAG, "disappear: speed=" + speed);
            Runnable r = new Runnable() {
                @Override
                public void run() {
                    int left = getLeft();
                    int right = getRight();
                    layoutWithAlpha(left + speed, getTop(), right + speed, getBottom());
                    Log.d(TAG, "run: layout");
                    if (left < screenWidth && right > 0) {
                        post(this);
                    } else {
                        if (mDisappearListener != null) {
                            mDisappearListener.onDisappear();
                        }
                    }
                }
            };
            post(r);
        }
    
        public void setOnDisappearListener(OnDisappearListener listener) {
            mDisappearListener = listener;
        }
    
        public void setOnClickNotificationListener(OnClickNotificationListener listener) {
            mClickNotificationListener = listener;
        }
    
    
        public static int getScreenWidth(Context context) {
            WindowManager wm = (WindowManager)
                    context.getSystemService(Context.WINDOW_SERVICE);
            return wm.getDefaultDisplay().getWidth();
        }
    
        /**
         * 通知滑动出去消失了的回调
         */
        public interface OnDisappearListener {
            void onDisappear();
        }
    
        /**
         * 点击通知的回调
         */
        public interface OnClickNotificationListener {
            void onClickNotification();
        }
    }
    

    在 activity 里面使用:

    public class MainActivity extends AppCompatActivity {
    
        private static final String TAG = "MainActivity";
    
        SwipeNotificationManager mWindowManager;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mWindowManager = SwipeNotificationManager.getInstance(this);
            Button addButton = (Button) findViewById(R.id.btn_add_notification);
            Button startActivityButton = (Button) findViewById(R.id.btn_start_activity);
            addButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    String text = "通知:" + String.valueOf(Math.random());
                    mWindowManager.addNotification(text, R.mipmap.ic_launcher);
                }
            });
    
            startActivityButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Intent intent = new Intent(MainActivity.this, MainActivity.class);
                    startActivity(intent);
                }
            });
    
        }
    

    最终效果图如下:


    悬浮通知.gif

    相关文章

      网友评论

          本文标题:一个能随手势滑动消除的 Android 应用内悬浮全局通知的实现

          本文链接:https://www.haomeiwen.com/subject/rqdqfftx.html