序言
前些日子跟朋友聊天狞贱,朋友Z果粉,前些天更新了微信蜀涨,說微信出了個好方便的功能啊瞎嬉,我問是啥功能啊蝎毡,看看我大Android有沒有,他說現(xiàn)在閱讀公眾號文章如果有人給你發(fā)微信你可以把這篇文章當(dāng)作懸浮窗懸浮起來佑颇,方便你聊完天不用找繼續(xù)閱讀顶掉,聽完是不是覺得這叫啥啊,我大Android微信版不是早就有這個功能了嗎挑胸,我看文章的時候看到過有這個懸浮按鈕痒筒,但是我一直沒有使用過,試了一下還是挺方便的茬贵,就想著自己實現(xiàn)一下這個功能簿透,下面看圖,大家都習(xí)慣了無圖言X
原理
看完動圖我們來分析一下解藻,如何在每個頁面上都存在一個View
呢老充,有些人可能會說,寫在base里面螟左,這樣每次啟動一個新的Activity
都要往頁面上addView
一次啡浊,性能不好,再說了胶背,我們作為一個優(yōu)秀的程序員能干這種重復(fù)的事嗎巷嚣,這種方案果斷打回去;既然這樣的話那我們肯定要在全局加了钳吟,那么全局是哪呢廷粒?相信了解過Activity
源碼的朋友肯定知道,全局可以在Window
層加啊红且,這樣既能一次性搞定坝茎,又不影響性能,說干就干暇番。
實現(xiàn)
1嗤放、權(quán)限
首先我們要考慮的一個問題就是權(quán)限問題,因為要適配Android 7.0 8.0
壁酬,添加懸浮窗是需要申請權(quán)限的次酌,這里參考了
Android 懸浮窗權(quán)限各機型各系統(tǒng)適配大全這篇文章,適配的比較全厨喂,可以直接拿來用和措。這里需要注意的是,為了適配Android 8.0
蜕煌,Window
的類型需要配置一下:
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;
}
2派阱、添加ViewGroup到Window
判斷好權(quán)限之后,直接添加就可以了
@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);
}
3斜纪、View的拖拽實現(xiàn)
借助WindowManager.LayoutParams
來實現(xiàn)贫母,mLayoutParams.x
和mLayoutParams.y
分別表示mView
左上角的橫縱坐標(biāo)文兑,所以我們只需要改動這兩個值就行了,當(dāng)ACTION_UP
時腺劣,計算當(dāng)前mView
的中心點相對窗口的位置绿贞,然后將mView
動態(tài)滑動到窗口左邊或者右邊:
//設(shè)置觸摸滑動事件
mView.setOnTouchListener(new View.OnTouchListener() {
int startX, startY; //起始點
boolean isMove; //是否在移動
long startTime;
int finalMoveX; //最后通過動畫將mView的X軸坐標(biāo)移動到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;
}
});
4橘原、注意
為了讓Window
與Activity
脫離籍铁,這里我們采用Service
來做,通過Service
來添加和移除View
趾断;在權(quán)限申請成功之后我們需要通知Service(其實是Activity拒名,可能會有保存數(shù)據(jù)等操作)作相應(yīng)改變(提供一個接口給Service),然后在Service中使用廣播來通知Activity芋酌;最后一個需要注意的地方就是我們需要判斷應(yīng)用程序是否在前臺還是后臺來添加或移除Window增显,這里通過使用ActivityLifecycleCallbacks來監(jiān)聽Activity在前臺的數(shù)量來判斷應(yīng)用程序是在前臺還是后臺
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", "應(yīng)用在前臺了!F甑邸同云!")
}
}
override fun onActivityDestroyed(activity: Activity?) {
}
override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) {
}
override fun onActivityStopped(activity: Activity?) {
started--
if (started == 0) {
Log.e("TAG", "應(yīng)用在后臺了!6赂埂炸站!")
}
}
override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) {
}
}
本文代碼已傳至Github,有需要的朋友可以下載下來看看秸滴。