Android仿微信文章懸浮窗效果

Android仿微信文章懸浮窗效果

序言 ?(轉(zhuǎn)載

閱讀公眾號(hào)文章如果有人給你發(fā)微信你可以把這篇文章當(dāng)作懸浮窗懸浮起來(lái),方便你聊完天不用找繼續(xù)閱讀,聽(tīng)完是不是覺(jué)得這叫啥啊,我大Android微信版不是早就有這個(gè)功能了嗎,我看文章的時(shí)候看到過(guò)有這個(gè)懸浮按鈕,但是我一直沒(méi)有使用過(guò),試了一下還是挺方便的,就想著自己實(shí)現(xiàn)一下這個(gè)功能,下面看圖,大家都習(xí)慣了無(wú)圖言X

原理

看完動(dòng)圖我們來(lái)分析一下,如何在每個(gè)頁(yè)面上都存在一個(gè)View呢,有些人可能會(huì)說(shuō),寫(xiě)在base里面,這樣每次啟動(dòng)一個(gè)新的Activity都要往頁(yè)面上addView一次,性能不好,再說(shuō)了,我們作為一個(gè)優(yōu)秀的程序員能干這種重復(fù)的事嗎,這種方案果斷打回去;既然這樣的話(huà)那我們肯定要在全局加了,那么全局是哪呢?相信了解過(guò)Activity源碼的朋友肯定知道,全局可以在Window層加啊,這樣既能一次性搞定,又不影響性能,說(shuō)干就干。

實(shí)現(xiàn)

1、權(quán)限

首先我們要考慮的一個(gè)問(wèn)題就是權(quán)限問(wèn)題,因?yàn)橐m配Android 7.0 8.0,添加懸浮窗是需要申請(qǐng)權(quán)限的,這里參考了?Android 懸浮窗權(quán)限各機(jī)型各系統(tǒng)適配大全這篇文章,適配的比較全,可以直接拿來(lái)用。這里需要注意的是,為了適配Android 8.0,Window的類(lèi)型需要配置一下:


?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的拖拽實(shí)現(xiàn)

借助WindowManager.LayoutParams來(lái)實(shí)現(xiàn),mLayoutParams.x和mLayoutParams.y分別表示mView左上角的橫縱坐標(biāo),所以我們只需要改動(dòng)這兩個(gè)值就行了,當(dāng)ACTION_UP時(shí),計(jì)算當(dāng)前mView的中心點(diǎn)相對(duì)窗口的位置,然后將mView動(dòng)態(tài)滑動(dòng)到窗口左邊或者右邊:


//設(shè)置觸摸滑動(dòng)事件

mView.setOnTouchListener(new View.OnTouchListener() {

? ? int startX, startY;? //起始點(diǎn)

? ? boolean isMove;? //是否在移動(dòng)

? ? long startTime;

? ? int finalMoveX;? //最后通過(guò)動(dòng)畫(huà)將mView的X軸坐標(biāo)移動(dòng)到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;

? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? //使用動(dòng)畫(huà)移動(dòng)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、注意

為了讓W(xué)indow與Activity脫離,這里我們采用Service來(lái)做,通過(guò)Service來(lái)添加和移除View;在權(quán)限申請(qǐng)成功之后我們需要通知Service(其實(shí)是Activity,可能會(huì)有保存數(shù)據(jù)等操作)作相應(yīng)改變(提供一個(gè)接口給Service),然后在Service中使用廣播來(lái)通知Activity;最后一個(gè)需要注意的地方就是我們需要判斷應(yīng)用程序是否在前臺(tái)還是后臺(tái)來(lái)添加或移除Window,這里通過(guò)使用ActivityLifecycleCallbacks來(lái)監(jiān)聽(tīng)Activity在前臺(tái)的數(shù)量來(lái)判斷應(yīng)用程序是在前臺(tái)還是后臺(tái)


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)用在前臺(tái)了?。?!")

? ? ? ? }

? ? }

? ? 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)用在后臺(tái)了?。?!")

? ? ? ? }

? ? }

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

? ? }

}


本文代碼已傳至Github

參考

Android 8.0 懸浮窗變動(dòng)與用法

Android 懸浮窗權(quán)限各機(jī)型各系統(tǒng)適配大全

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 序言 前些日子跟朋友聊天,朋友Z果粉,前些天更新了微信,說(shuō)微信出了個(gè)好方便的功能啊,我問(wèn)是啥功能啊,看看我大And...
    24K純帥豆閱讀 6,899評(píng)論 2 31
  • 用兩張圖告訴你,為什么你的 App 會(huì)卡頓? - Android - 掘金 Cover 有什么料? 從這篇文章中你...
    hw1212閱讀 13,934評(píng)論 2 59
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,872評(píng)論 25 709
  • 20歲。是一個(gè)怎么樣年齡,想探尋的太多,想嘗試的太多,害怕的茫然不知所措的也太多太多。 在大學(xué)里,...
    范星星閱讀 335評(píng)論 0 1
  • 近來(lái)二十幾年間,藥物常年在身邊; 究其原因歸何處?根源就在大米中。 萬(wàn)家陳米恒食中,毒素慢慢侵身體; 吾勸諸君當(dāng)立...
    傲梅寒雪閱讀 290評(píng)論 0 0

友情鏈接更多精彩內(nèi)容