Android仿微信文章懸浮窗效果

序言

前些日子跟朋友聊天,朋友Z果粉,前些天更新了微信,說微信出了個好方便的功能啊,我問是啥功能啊,看看我大Android有沒有,他說如今閱讀公衆號文章若是有人給你發微信你能夠把這篇文章看成懸浮窗懸浮起來,方便你聊完天不用找繼續閱讀,聽完是否是以爲這叫啥啊,我大Android微信版不是早就有這個功能了嗎,我看文章的時候看到過有這個懸浮按鈕,可是我一直沒有使用過,試了一下仍是挺方便的,就想着本身實現一下這個功能,下面看圖,你們都習慣了無圖言X前端

image

原理

看完動圖咱們來分析一下,如何在每一個頁面上都存在一個View呢,有些人可能會說,寫在base裏面,這樣每次啓動一個新的Activity都要往頁面上addView一次,性能很差,再說了,咱們做爲一個優秀的程序員能幹這種重複的事嗎,這種方案果斷打回去;既然這樣的話那咱們確定要在全局加了,那麼全局是哪呢?相信瞭解過Activity源碼的朋友確定知道,全局能夠在Window層加啊,這樣既能一次性搞定,又不影響性能,說幹就幹。git

實現

一、權限

首先咱們要考慮的一個問題就是權限問題,由於要適配Android 7.0 8.0,添加懸浮窗是須要申請權限的,這裏參考了[
Android 懸浮窗權限各機型各系統適配大全](https://blog.csdn.net/self_st...,適配的比較全,能夠直接拿來用。這裏須要注意的是,爲了適配Android 8.0Window的類型須要配置一下:程序員

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
    //Android 8.0
    mLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
    //其餘版本
    mLayoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
}

二、添加ViewGroup到Window

判斷好權限以後,直接添加就能夠了github

@SuppressLint("CheckResult")
private void showWindow(Context context) {
    mWindowManager = (WindowManager) context.getSystemService(WINDOW_SERVICE);
    mView = LayoutInflater.from(context).inflate(R.layout.article_window, null);

    ImageView ivImage = mView.findViewById(R.id.aw_iv_image);
    String imageUrl = SPUtil.getStringDefault(ARTICLE_IMAGE_URL, "");
    RequestOptions requestOptions = RequestOptions.circleCropTransform();
    requestOptions.placeholder(R.mipmap.ic_launcher_round).error(R.mipmap.ic_launcher_round);
    Glide.with(context).load(imageUrl).apply(requestOptions).into(ivImage);

    initListener(context);

    mLayoutParams = new WindowManager.LayoutParams();
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        mLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
    } else {
        mLayoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
    }
    mLayoutParams.format = PixelFormat.RGBA_8888;   //窗口透明
    mLayoutParams.gravity = Gravity.LEFT | Gravity.TOP;  //窗口位置
    mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
    mLayoutParams.width = 200;
    mLayoutParams.height = 200;
    mLayoutParams.x = mWindowManager.getDefaultDisplay().getWidth() - 200;
    mLayoutParams.y = 0;
    mWindowManager.addView(mView, mLayoutParams);
}

三、View的拖拽實現

藉助WindowManager.LayoutParams來實現,mLayoutParams.xmLayoutParams.y分別表示mView左上角的橫縱座標,因此咱們只須要改動這兩個值就好了,當ACTION_UP時,計算當前mView的中心點相對窗口的位置,而後將mView動態滑動到窗口左邊或者右邊:微信

//設置觸摸滑動事件
mView.setOnTouchListener(new View.OnTouchListener() {
    int startX, startY;  //起始點
    boolean isMove;  //是否在移動
    long startTime;
    int finalMoveX;  //最後經過動畫將mView的X軸座標移動到finalMoveX

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startX = (int) event.getX();
                startY = (int) event.getY();
                startTime = System.currentTimeMillis();
                isMove = false;
                return false;
            case MotionEvent.ACTION_MOVE:
                mLayoutParams.x = (int) (event.getRawX() - startX);
                mLayoutParams.y = (int) (event.getRawY() - startY);
                updateViewLayout();   //更新mView 的位置
                return true;
            case MotionEvent.ACTION_UP:
                long curTime = System.currentTimeMillis();
                isMove = curTime - startTime > 100;
                
                //判斷mView是在Window中的位置,以中間爲界
                if (mLayoutParams.x + mView.getMeasuredWidth() / 2 >= mWindowManager.getDefaultDisplay().getWidth() / 2) {
                    finalMoveX = mWindowManager.getDefaultDisplay().getWidth() - mView.getMeasuredWidth();
                } else {
                    finalMoveX = 0;
                }
                
                //使用動畫移動mView
                ValueAnimator animator = ValueAnimator.ofInt(mLayoutParams.x, finalMoveX).setDuration(Math.abs(mLayoutParams.x - finalMoveX));
                animator.addUpdateListener((ValueAnimator animation) -> {
                    mLayoutParams.x = (int) animation.getAnimatedValue();
                    updateViewLayout();
                });
                animator.start();

                return isMove;
        }
        return false;
    }
});

四、注意

爲了讓WindowActivity脫離,這裏咱們採用Service來作,經過Service來添加和移除View;在權限申請成功以後咱們須要通知Service(實際上是Activity,可能會有保存數據等操做)做相應改變(提供一個接口給Service),而後在Service中使用廣播來通知Activity;最後一個須要注意的地方就是咱們須要判斷應用程序是否在前臺仍是後臺來添加或移除Window,這裏經過使用ActivityLifecycleCallbacks來監聽Activity在前臺的數量來判斷應用程序是在前臺仍是後臺app

class ApplicationLifecycle : Application.ActivityLifecycleCallbacks {

    private var started: Int = 0

    override fun onActivityPaused(activity: Activity?) {
    }

    override fun onActivityResumed(activity: Activity?) {
    }

    override fun onActivityStarted(activity: Activity?) {
        started++
        if (started == 1) {
            Log.e("TAG", "應用在前臺了!!!")
        }
    }

    override fun onActivityDestroyed(activity: Activity?) {
    }

    override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) {
    }

    override fun onActivityStopped(activity: Activity?) {
        started--
        if (started == 0) {
            Log.e("TAG", "應用在後臺了!!!")
        }
    }

    override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) {
    }
}

本文代碼已傳至Github,有須要的朋友能夠下載下來看看。ide

參考

公衆號

歡迎關注個人我的公衆號【IT先森養成記】,專一大前端技術分享,包含Android,Java基礎,Kotlin,HTML,CSS,JS等技術;在這裏你能獲得的不止是技術上的提高,還有一些學習經驗以及志同道合的朋友,趕快加入咱們,一塊兒學習,一塊兒進化吧!!!性能

公衆號:IT先森養成記

相關文章
相關標籤/搜索