一、小窗模式
1.1 小窗功能的開啟方式
- 開發者模式下開啟小窗功能
- adb 手動開啟
adb shell settings put global enable_freeform_support 1
adb shell settings put global force_resizable_activities 1
1.2 源碼配置
- copy file
# add for freedom
PRODUCT_COPY_FILES += \frameworks/native/data/etc/android.software.freeform_window_management.xml:$(TARGET_COPY_OUT_SYSTEM)/etc/permissions/android.software.freeform_window_management.xml
- overlay
<!-- add for freeform --><bool name="config_freeformWindowManagement">true</bool>
1.3 小窗的啟動方式
主要的啟動方式,一個是多任務里面,點擊應用圖標,選擇小窗模式,另一個是通過三方應用啟動,比如側邊欄,通知欄等待。
- 三方應用通過ActivityOptions 啟動
public void startFreeFormActivity(View view) {Intent intent = new Intent(this, FreeFormActivity.class);ActivityOptions options = ActivityOptions.makeBasic();options.setLaunchWindowingMode(WINDOWING_MODE_FREEFORM);startActivity(intent, options.toBundle());}
- 多任務啟動
通過長按應用圖標,選擇小窗模式,ActivityOptions 會設置 ActivityOptions#setLaunchWindowingMode 為 WINDOWING_MODE_FREEFORM,然后通過 ActivityManager#startActivity 啟動 Activity。
frameworks/base/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java
public class ActivityTaskSupervisor implements RecentTasks.Callbacks {int startActivityFromRecents(int callingPid, int callingUid, int taskId,SafeActivityOptions options) {...代碼省略... }
}
1.4 應用兼容
應用需要設置 android:resizeableActivity=“true”,應用安裝過程中會解析AndroidManifest.xml,并設置 PackageParser.ActivityInfo 的 privateFlags,在啟動應用的時候,會根據 privateFlags 的值來判斷是否支持小窗。
if (sa.hasValueOrEmpty(R.styleable.AndroidManifestApplication_resizeableActivity)) {if (sa.getBoolean(R.styleable.AndroidManifestApplication_resizeableActivity, true)) {ai.privateFlags |= PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE;} else {ai.privateFlags |= PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_UNRESIZEABLE;}} else if (owner.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.N) {ai.privateFlags |= PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION;}
1.5 基礎的窗口信息
在Android 系統中,窗口是應用程序界面的基本單元,用于承載和顯示應用的視圖內容。每個 Activity 都有一個主窗口,但也可以有其他窗口,如對話框、懸浮窗等。
在應用上層的一些組件的體現上,變化不是很大,但是 Framework 中的對于窗口的管理,迭代變化一直都比較大,可能之前有的類,新的架構下就被精簡了,所以主要掌握窗口的一些概念,這樣比較容易在新的架構之下找到對應的實現。
這里先回顧一下基礎的窗口和界面相關的概念:
- Window(窗口): 窗口是應用程序界面的基本單元,用于承載和顯示應用的視圖內容。每個 Activity 都有一個主窗口,但也可以有其他窗口,如對話框、懸浮窗等。
- WindowManagerService: 是 Android 系統中的窗口管理服務,負責管理窗口的創建、顯示、移動、調整大小、層級關系等。它是 Android 窗口系統的核心組件。
- View(視圖): View 是 Android 中用戶界面的基本構建塊,用于在窗口中繪制和顯示內容。它是窗口中可見元素的基礎。
- ViewGroup(視圖組): ViewGroup 是一種特殊的 View,它可以包含其他視圖(包括 View 和其他 ViewGroup)來形成復雜的用戶界面。
- Surface(表面): Surface 是用于繪制圖形內容的區域,窗口和視圖內容都可以在 Surface 上繪制。每個窗口通常對應一個 Surface。
- SurfaceFlinger: 是 Android 系統中的一個組件,負責管理和合成窗口中的 Surface,以及在屏幕上繪制這些 Surface。
- LayoutParams(布局參數): LayoutParams 是窗口或視圖的布局參數,用于指定視圖在其父視圖中的位置、大小和外觀等。
- Window Token(窗口令牌): Window Token 是一個用于標識窗口所屬于的應用程序或任務的對象。它在窗口的顯示和交互中起著重要作用。
- Window Decor(窗口裝飾): Window Decor 是窗口的裝飾元素,如標題欄、狀態欄等,可以影響窗口的外觀和交互。
- Dialog(對話框): 對話框是一種特殊的窗口,用于在當前活動之上顯示臨時的提示、選擇或輸入內容。
- Activity(活動): 在 Android 應用程序中,每個 Activity 都與一個 PhoneWindow 相關聯。PhoneWindow 用于管理 Activity 的界面繪制和交互。
- Window Callback(窗口回調): PhoneWindow 實現了 Window.Callback 接口,該接口用于處理窗口事件和交互。通過實現這個接口,您可以監聽和響應窗口的狀態變化、輸入事件等。
- DecorView(裝飾視圖): PhoneWindow 中的內容通常由 DecorView 承載。DecorView 是一個特殊的 ViewGroup,用于包含應用程序的用戶界面內容和窗口裝飾元素,如標題欄、狀態欄等。
- PhoneWindow(應用程序窗口): PhoneWindow 是 android.view.Window 類的實現之一,用于表示一個應用程序窗口。它提供了窗口的基本功能,如繪制、布局、裝飾、焦點管理等。
1.6 窗口類型
frameworks/base/core/java/android/app/WindowConfiguration.java
public class WindowConfiguration implements Parcelable, Comparable<WindowConfiguration> {public static final int WINDOWING_MODE_UNDEFINED = 0;public static final int WINDOWING_MODE_FULLSCREEN = 1;//全屏窗口模式public static final int WINDOWING_MODE_PINNED = 2;//固定窗口模式public static final int WINDOWING_MODE_SPLIT_SCREEN_PRIMARY = 3;public static final int WINDOWING_MODE_SPLIT_SCREEN_SECONDARY = 4;public static final int WINDOWING_MODE_FREEFORM = 5;//自由窗口模式public static final int WINDOWING_MODE_MULTI_WINDOW = 6;//多窗口模式/** @hide */@IntDef(prefix = { "WINDOWING_MODE_" }, value = {WINDOWING_MODE_UNDEFINED,WINDOWING_MODE_FULLSCREEN,WINDOWING_MODE_MULTI_WINDOW,WINDOWING_MODE_PINNED,WINDOWING_MODE_SPLIT_SCREEN_PRIMARY,WINDOWING_MODE_SPLIT_SCREEN_SECONDARY,WINDOWING_MODE_FREEFORM,})public @interface WindowingMode {}
}
二、小窗的創建
小窗的創建時機有兩塊,一塊是在頁面創建的時候,也就是PhoneWindow創建的時候,會創建DecorView,DecorView 會判斷是否要創立一個 DecorCaptionView。 另外一塊當DecorView動態變化的時候,當有參數變量,經過onWindowSystemUiVisibilityChanged方法回調或者 onConfigurationChanged方法回調,系統會對DecorView 進行更新并判斷是否需要新建一個小窗DecorCaptionView的視圖。
2.1 頁面創建的時候創建小窗
>frameworks/base/core/java/android/app/Activity.java
public class Activity extends ContextThemeWrapperimplements LayoutInflater.Factory2,Window.Callback, KeyEvent.Callback,OnCreateContextMenuListener, ComponentCallbacks2,Window.OnWindowDismissedCallback,ContentCaptureManager.ContentCaptureClient {public void setContentView(View view) {//注釋1,調用PhoneWindow的setContentView方法getWindow().setContentView(view);initWindowDecorActionBar();}}>frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
public class PhoneWindow extends Window implements MenuBuilder.Callback {private DecorView mDecor;ViewGroup mContentParent;@Overridepublic void setContentView(View view, ViewGroup.LayoutParams params) {if (mContentParent == null) {//注釋2,調用installDecorinstallDecor();} ...代碼省略...}private void installDecor() {...代碼省略...//注釋3,創建窗口對應的DecorView視圖mDecor = generateDecor(-1);...代碼省略...//注釋4,調用generateLayout方法mContentParent = generateLayout(mDecor); }protected ViewGroup generateLayout(DecorView decor) {...代碼省略...//注釋5,調用DecorView的onResourcesLoaded方法mDecor.onResourcesLoaded(mLayoutInflater, layoutResource); ...代碼省略... }
}>frameworks/base/core/java/com/android/internal/policy/DecorView.java
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {//加載應用需要的布局資源void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {...代碼省略...//創建小窗視圖mDecorCaptionView = createDecorCaptionView(inflater);//應用需要加載的根布局final View root = inflater.inflate(layoutResource, null);if (mDecorCaptionView != null) {//注釋6,如果小窗視圖不為空,則將小窗的視圖添加到DecorView中if (mDecorCaptionView.getParent() == null) {addView(mDecorCaptionView,new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));}//然后將應用根布局添加到mDecorCaptionView中mDecorCaptionView.addView(root,new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));} else {//注釋7,如果不是小窗,則將應用根布局添加到DecorView中addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));}mContentRoot = (ViewGroup) root;initializeElevation();}private DecorCaptionView createDecorCaptionView(LayoutInflater inflater) {DecorCaptionView decorCaptionView = null;for (int i = getChildCount() - 1; i >= 0 && decorCaptionView == null; i--) {View view = getChildAt(i);if (view instanceof DecorCaptionView) {// The decor was most likely saved from a relaunch - so reuse it.decorCaptionView = (DecorCaptionView) view;removeViewAt(i);}}final WindowManager.LayoutParams attrs = mWindow.getAttributes();final boolean isApplication = attrs.type == TYPE_BASE_APPLICATION ||attrs.type == TYPE_APPLICATION || attrs.type == TYPE_DRAWN_APPLICATION;final WindowConfiguration winConfig = getResources().getConfiguration().windowConfiguration;// Only a non floating application window on one of the allowed workspaces can get a captionif (!mWindow.isFloating() && isApplication && winConfig.hasWindowDecorCaption()) {// Dependent on the brightness of the used title we either use the// dark or the light button frame.if (decorCaptionView == null) {//調用inflateDecorCaptionView方法decorCaptionView = inflateDecorCaptionView(inflater);}decorCaptionView.setPhoneWindow(mWindow, true /*showDecor*/);} else {decorCaptionView = null;}enableCaption(decorCaptionView != null);return decorCaptionView;}private DecorCaptionView inflateDecorCaptionView(LayoutInflater inflater) {final Context context = getContext();inflater = inflater.from(context);//注釋8,構建小窗對應的布局文件final DecorCaptionView view = (DecorCaptionView) inflater.inflate(R.layout.decor_caption,null);setDecorCaptionShade(view);return view;}
}
如上所示Activity的setContentView方法經過層層調用最終會觸發DecorView的onResourcesLoaded方法,如果是小窗模式,會走到注釋6處,將頁面需要的布局資源添加到DecorCaptionView中,然后將DecorCaptionView添加到DecorView中;如果是普通應用場景,會走到注釋7處,會將布局資源添加到DecorView中;最終WMS會將DecorView添加到屏幕上。另外在注釋8處可以看到小窗加載的是什么布局資源。
2.2 在DecorView的回調方法中創建小窗
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {@Overridepublic void onWindowSystemUiVisibilityChanged(int visible) {updateColorViews(null /* insets */, true /* animate */);//調用updateDecorCaptionStatus方法更新小窗的狀態updateDecorCaptionStatus(getResources().getConfiguration());if (mStatusGuard != null && mStatusGuard.getVisibility() == VISIBLE) {updateStatusGuardColor();}}@Overrideprotected void onConfigurationChanged(Configuration newConfig) {super.onConfigurationChanged(newConfig);//調用updateDecorCaptionStatus方法更新小窗的狀態updateDecorCaptionStatus(newConfig);initializeElevation();}private void updateDecorCaptionStatus(Configuration config) {//如果定義窗口類型為小窗,且不是全屏模式, 則創建一個DecorCaptionView在DecorView內部。final boolean displayWindowDecor = config.windowConfiguration.hasWindowDecorCaption()&& !isFillingScreen(config);if (mDecorCaptionView == null && displayWindowDecor) {// Configuration now requires a caption.final LayoutInflater inflater = mWindow.getLayoutInflater();//注釋1,調用createDecorCaptionView方法創建小窗視圖mDecorCaptionView = createDecorCaptionView(inflater);if (mDecorCaptionView != null) {if (mDecorCaptionView.getParent() == null) {addView(mDecorCaptionView, 0,new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));}removeView(mContentRoot);mDecorCaptionView.addView(mContentRoot,new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));}} else if (mDecorCaptionView != null) {//注釋2,如果已經創建了 DecorCaptionView, 則更新配置信息,比如窗口大小,窗口位置等。mDecorCaptionView.onConfigurationChanged(displayWindowDecor);// 是否顯示小窗的標題欄enableCaption(displayWindowDecor);}}
}
在DecorView的onWindowSystemUiVisibilityChanged回調方法和onConfigurationChanged回調方法中,都會進一步調用updateDecorCaptionStatus方法;然后如果判斷需要創建小窗且小窗還未被創建,則在注釋1處,會調用createDecorCaptionView方法創建小窗視圖;否則在注釋2處,調用DecorCaptionView的onConfigurationChanged方法通知小窗進行配置變化。
三、小窗的實現
2.2 小窗的標題欄
frameworks/base/core/java/com/android/internal/widget/DecorCaptionView.java
public class DecorCaptionView extends ViewGroup implements View.OnTouchListener,GestureDetector.OnGestureListener {private View mCaption; // 標題欄private View mContent; // 小窗View之下,應用的根Viewprivate View mMaximize; // 最大化按鈕private View mClose; // 關閉按鈕
}
2.3 觸摸事件
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {//如果在應用的小窗View外部點擊的話,直接將事件攔截掉,這樣就不會觸發應用的點擊事件。@Overridepublic boolean onInterceptTouchEvent(MotionEvent event) {int action = event.getAction();//是否顯示小窗標題欄if (mHasCaption && isShowingCaption()) {//如果窗口可調整大小,并且事件是(開始)在小窗外部,則不要將 ACTION_DOWN 事件進行傳遞。窗口調整大小事件應由WindowManager處理。if (action == MotionEvent.ACTION_DOWN) {final int x = (int) event.getX();final int y = (int) event.getY();if (isOutOfInnerBounds(x, y)) {return true;}}}...代碼省略...return false;}
}
frameworks/base/core/java/com/android/internal/widget/DecorCaptionView.java
public class DecorCaptionView extends ViewGroup implements View.OnTouchListener,GestureDetector.OnGestureListener {private View mClickTarget;@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {//如果用戶點擊最大化或者關閉按鈕,就攔截事件,這樣就不會觸發應用的點擊事件if (ev.getAction() == MotionEvent.ACTION_DOWN) {final int x = (int) ev.getX();final int y = (int) ev.getY();//Only offset y for containment tests because the actual views are already translated.//是否點擊到最大化按鈕的區域if (mMaximizeRect.contains(x, y - mRootScrollY)) {mClickTarget = mMaximize;}//是否點擊到關閉按鈕的區域if (mCloseRect.contains(x, y - mRootScrollY)) {mClickTarget = mClose;}}return mClickTarget != null;}}
在小窗的View中,應用就不能通過View的事件來直接處理,DecorCaptionView 需要通過計算View所在的矩形區域,然后計算點擊的區域是否處于該矩形區域范圍內判斷為點擊,就是一個點和面的問題,原生小窗上的問題就是這個觸控面太小,手指的點擊區域可能不容易觸發。 (在我開發的ChatDev游戲中,也有面和面碰撞的問題,玩家的位置和碰撞位置的計算)
2.4 小窗的邊界
目前國內的廠商的小窗設計,都是通過在DecorView里面模仿DecorCaptionView自定義一個View,用來作為小窗內部應用的容器。
-
邊界圓角 一般會對這個小窗容器進行一個UI上的美化,主要的一個就是邊界的圓角輪廓繪制。
-
導航欄重疊的問題
DisplayPolicy 作為系統里面控制顯示 dock欄、狀態欄、導航欄的樣式的主要類, 在每次繪制布局之后,都會走到如下applyPostLayoutPolicyLw 函數,進行顯示規則的條件,當判斷重疊之后,在導航欄更新透明度規則的時候,將其標記中不透明的純深色背景和淺色前景清空。
public class DisplayPolicy {public void applyPostLayoutPolicyLw(WindowState win, WindowManager.LayoutParams attrs,WindowState attached, WindowState imeTarget) {final boolean affectsSystemUi = win.canAffectSystemUiFlags();if (DEBUG_LAYOUT) Slog.i(TAG, "Win " + win + ": affectsSystemUi=" + affectsSystemUi);applyKeyguardPolicy(win, imeTarget);// 檢查自由窗口是否與導航欄區域重疊。final boolean isOverlappingWithNavBar = isOverlappingWithNavBar(win);if (isOverlappingWithNavBar && !mIsFreeformWindowOverlappingWithNavBar&& win.inFreeformWindowingMode()) {// 如果窗口是自由窗口,并且窗口和導航欄重疊mIsFreeformWindowOverlappingWithNavBar = true;}if (!affectsSystemUi) {return;}...代碼省略...}}
💡 技術無價,贊賞隨心
寫文不易,如果本文幫你避開了“八小時踩坑”,或者讓你直呼“學到了!”
歡迎掃碼贊賞,讓我知道這篇內容值得!