由于項(xiàng)目需求歌径,需要實(shí)現(xiàn)一個(gè)應(yīng)用內(nèi)懸浮窗功能验庙,要求是:
- 點(diǎn)擊懸浮窗圖標(biāo)可以跳轉(zhuǎn)到一個(gè)固定的Activity A袜刷;
- 用戶在app內(nèi)的任一activity里都能看到懸浮窗(除了Activity A)
- 懸浮窗可以關(guān)閉议经,可以拖動(dòng)横堡,拖動(dòng)過程中松手可以自動(dòng)吸附到屏幕邊緣埋市。
經(jīng)過搜索,我發(fā)現(xiàn)了一個(gè)第三方庫EasyFloat(https://github.com/princekin-f/EasyFloat)翅萤,功能相當(dāng)強(qiáng)大恐疲。
EasyFloat支持四種懸浮窗:
類型 | 特點(diǎn) | 是否需要懸浮窗權(quán)限 |
---|---|---|
CURRENT_ACTIVITY | 只在當(dāng)前Activity顯示 | 否 |
FOREGROUND | 僅應(yīng)用前臺時(shí)顯示 | 是 |
BACKGROUND | 僅應(yīng)用后臺時(shí)顯示 | 是 |
ALL_TIME | 一直顯示(不分前后臺) | 是 |
那么問題來了,我要實(shí)現(xiàn)的功能是應(yīng)用內(nèi)的懸浮窗套么,回到桌面后沒必要繼續(xù)顯示懸浮窗培己。所以我并不想申請懸浮窗權(quán)限。何況由于不同安卓手機(jī)系統(tǒng)的限制胚泌,懸浮窗權(quán)限沒有一個(gè)統(tǒng)一的申請方式省咨,需要針對不同手機(jī)系統(tǒng)做單獨(dú)適配,測試起來也麻煩玷室。
既然CURRENT_ACTIVITY
不需要權(quán)限零蓉,那么有沒有辦法通過CURRENT_ACTIVITY
來實(shí)現(xiàn)應(yīng)用內(nèi)全局懸浮窗呢穷缤?
思路:
- 每打開一個(gè)activity敌蜂,就創(chuàng)建一個(gè)
CURRENT_ACTIVITY
類型的懸浮窗,如果創(chuàng)建的速度足夠快津肛,用戶會以為所有界面的懸浮窗都是同一個(gè)章喉; - 拖動(dòng)懸浮窗后,記錄下它的坐標(biāo)。當(dāng)跳轉(zhuǎn)到別的activity時(shí)秸脱,將新創(chuàng)建的懸浮窗的位置設(shè)置成已保存的坐標(biāo)落包。
經(jīng)過測試,這個(gè)方案完全可行摊唇。以下是實(shí)現(xiàn)步驟:
1. 實(shí)現(xiàn)懸浮窗界面:
App.java:
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
EasyFloat.init(this, true);
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
}
@Override
public void onActivityStarted(@NonNull Activity activity) {
}
@Override
public void onActivityResumed(@NonNull Activity activity) {
doOnActivityResume(activity);
}
@Override
public void onActivityPaused(@NonNull Activity activity) {
}
@Override
public void onActivityStopped(@NonNull Activity activity) {
doOnActivityStopped(activity);
}
@Override
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
}
});
}
private static final String TAG_FLOAT_VIEW = "TAG_FLOAT_VIEW";
private void doOnActivityResume(Activity activity) {
EasyFloat.Builder builder = EasyFloat.with(activity)
.setLayout(R.layout.layout_float_window)
.setShowPattern(ShowPattern.CURRENT_ACTIVITY)
.setAnimator(null) /*關(guān)閉動(dòng)畫*/
.setSidePattern(SidePattern.RESULT_HORIZONTAL) /*吸附效果*/
.setTag(TAG_FLOAT_VIEW);
builder.setGravity(Gravity.RIGHT | Gravity.CENTER_VERTICAL);
builder.show();
}
private void doOnActivityStopped(Activity activity) {
EasyFloat.dismiss(activity, TAG_FLOAT_VIEW);//關(guān)閉前一個(gè)界面的懸浮窗
}
}
layout_float_window.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_float_view">
<TextView
android:id="@+id/click_view_float"
android:layout_width="100dp"
android:layout_height="100dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:gravity="center"
android:text="點(diǎn)我"
android:textColor="#fff"
android:textSize="24sp">
</TextView>
<ImageView
android:id="@+id/click_view_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignRight="@id/click_view_float"
android:layout_alignParentTop="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:padding="3dp"
android:src="@drawable/ic_close_circle">
</ImageView>
</RelativeLayout>
實(shí)現(xiàn)效果:
期間遇到一個(gè)編譯錯(cuò)誤:
查詢資料得知咐蝇,這是由于EasyFloat是kotlin實(shí)現(xiàn)的,而我的工程是java開發(fā)的巷查,根據(jù) https://www.cnblogs.com/yubo0522/p/11534474.html有序, 加上kotlin相關(guān)支持即可。
2. 實(shí)現(xiàn)點(diǎn)擊事件
private static final String TAG_FLOAT_VIEW = "TAG_FLOAT_VIEW";
private void doOnActivityResume(Activity activity) {
//客服界面不彈出懸浮窗
if (activity instanceof CustomerServiceActivity){
return;
}
EasyFloat.Builder builder = EasyFloat.with(activity)
.setLayout(R.layout.layout_float_window, new OnInvokeView() {
@Override
public void invoke(View view) {
View click_view_float = view.findViewById(R.id.click_view_float);
View click_view_close = view.findViewById(R.id.click_view_close);
click_view_float.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
CustomerServiceActivity.start(view.getContext());
}
});
click_view_close.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dismissFloatView(activity);
}
});
}
})
.setShowPattern(ShowPattern.CURRENT_ACTIVITY)
.setAnimator(null)
.setSidePattern(SidePattern.RESULT_HORIZONTAL)
.setTag(TAG_FLOAT_VIEW);
builder.setGravity(Gravity.RIGHT | Gravity.CENTER_VERTICAL);
builder.show();
}
private void dismissFloatView(Activity activity) {
EasyFloat.dismiss(activity, TAG_FLOAT_VIEW);
}
private void doOnActivityStopped(Activity activity) {
dismissFloatView(activity);
}
效果圖:
3. 拖動(dòng)后記錄位置
App.java:
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
EasyFloat.init(this, true);
registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) {
}
@Override
public void onActivityStarted(@NonNull Activity activity) {
}
@Override
public void onActivityResumed(@NonNull Activity activity) {
doOnActivityResume(activity);
}
@Override
public void onActivityPaused(@NonNull Activity activity) {
}
@Override
public void onActivityStopped(@NonNull Activity activity) {
doOnActivityStopped(activity);
}
@Override
public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
}
});
}
private static final String TAG_FLOAT_VIEW = "TAG_FLOAT_VIEW";
private static int floatXOffset;
private static int floatYOffset;
private static boolean hasDragged = false;
private void doOnActivityResume(Activity activity) {
//客服界面不彈出懸浮窗
if (activity instanceof CustomerServiceActivity) {
return;
}
EasyFloat.Builder builder = EasyFloat.with(activity)
.setLayout(R.layout.layout_float_window, new OnInvokeView() {
@Override
public void invoke(View view) {
View click_view_float = view.findViewById(R.id.click_view_float);
View click_view_close = view.findViewById(R.id.click_view_close);
click_view_float.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
CustomerServiceActivity.start(view.getContext());
}
});
click_view_close.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dismissFloatView(activity);
}
});
}
})
.setShowPattern(ShowPattern.CURRENT_ACTIVITY)
.setAnimator(null)
.setSidePattern(SidePattern.RESULT_HORIZONTAL)
.setTag(TAG_FLOAT_VIEW);
if (!hasDragged) {
builder.setGravity(Gravity.RIGHT | Gravity.CENTER_VERTICAL);
} else {
builder.setLocation(floatXOffset, floatYOffset);
}
builder.registerCallbacks(new OnFloatCallbacks() {
@Override
public void createdResult(boolean b, @org.jetbrains.annotations.Nullable String s, @org.jetbrains.annotations.Nullable View view) {
}
@Override
public void show(@NotNull View view) {
}
@Override
public void hide(@NotNull View view) {
}
@Override
public void dismiss() {
}
@Override
public void touchEvent(@NotNull View view, @NotNull MotionEvent motionEvent) {
}
@Override
public void drag(@NotNull View view, @NotNull MotionEvent motionEvent) {
}
@Override
public void dragEnd(@NotNull View view) {
//拖動(dòng)結(jié)束
hasDragged = true;
saveFloatViewPosition(activity);
}
});
builder.show();
}
private void saveFloatViewPosition(Activity activity) {
View floatView = EasyFloat.getFloatView(activity, TAG_FLOAT_VIEW);
if (floatView != null) {
//獲取懸浮窗的位置
int[] outLocation = new int[2];
floatView.getLocationInWindow(outLocation);
floatXOffset = outLocation[0];
floatYOffset = outLocation[1] - Utils.getToolbarHeight(floatView.getContext()) - Utils.getStatusBarHeight(activity);
}
}
private void dismissFloatView(Activity activity) {
EasyFloat.dismiss(activity, TAG_FLOAT_VIEW);
}
private void doOnActivityStopped(Activity activity) {
dismissFloatView(activity);
}
}
Utils.java:
public class Utils {
public static int getStatusBarHeight(Context context) {
int result = 0;
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
result = context.getResources().getDimensionPixelSize(resourceId);
}
return result;
}
public static int getToolbarHeight(Context context) {
TypedValue tv = new TypedValue();
if (context.getTheme().resolveAttribute(android.R.attr.actionBarSize, tv, true)) {
int height = TypedValue.complexToDimensionPixelSize(tv.data, context.getResources().getDisplayMetrics());
return height;
}
return 0;
}
}
最終實(shí)現(xiàn)效果: