小窗模式,作為一種在移動裝置上的多工處理方式,為使用者帶來了便捷和高效的體驗,尤其在一些特定場景下,其價值愈發凸顯。以下是為什麼需要小窗模式的一些重要原因:
內容複製和跨應用操作: 小窗模式允許使用者輕鬆從一個應用(A應用)複製內容到另一個應用(B應用),而無需頻繁切換應用。這在遷移微信聊天記錄或在不同應用之間共用資訊時非常有用。使用者可以在小視窗中檢視和編輯資訊,然後將其貼上到目標應用,這大大提高了效率。
無縫切換和即用即走: 在某些情況下,使用者可能需要臨時使用一個應用程式(B應用),而不想完全離開當前應用程式(A應用)。小窗模式允許使用者在不中斷A應用的情況下快速存取B應用,然後無縫返回A應用。這對於檢視實時資訊或快速執行任務非常有用,如叫滴滴後檢視司機位置。
多工處理和注意力分配: 當用戶需要在一個應用程式(A應用)中保持關注,並且還需要時不時地檢查另一個應用程式(B應用)的更新時,小窗模式非常有用。使用者可以將B應用以小視窗的形式浮動在A應用上方,無需頻繁切換,從而更好地分配注意力,減輕焦慮情緒。
快速回復和記錄: 在看網課、玩遊戲或執行其他任務時,使用者可能會收到訊息或需要記錄筆記。小窗模式允許使用者在小視窗中輕鬆回覆訊息或記筆記,而不必退出當前應用程式。這提高了多工處理的效率。
心理負擔減輕: 頻繁切換應用程式可能會導致使用者分散注意力,增加心理負擔,甚至產生焦慮情緒。小窗模式的引入可以減輕這種心理負擔,使使用者更輕鬆地處理多個任務。
總的來說,小窗模式為使用者提供了更靈活、高效和愉悅的應用程式管理和多工處理方式。在日常生活和工作中,小窗模式可以極大地提高使用者的生產力和使用者體驗,成為了現代移動裝置不可或缺的功能之一。不僅如此,眾多Android作業系統製造商也在不斷迭代和優化小窗模式,以滿足使用者不斷變化的需求。
本文主要是對原生的自由視窗模式進行一個程式碼分析,具體各家的小窗效果,可以參考這篇文章:
小米、華為、OPPO……五大 Android 系統橫向對比,誰的「小窗模式」最好用?
基於 SourceCodeTrace 專案推崇的原則,本文程式碼塊參照均有來源,SourceCodeTrace Project 幫助您在部落格、文章記錄的過程中,引入對應專案以及版本,行號等資訊,讓後續的讀者,通過參照來源,能夠進行更加深入的學習,在部落格或文章中引入程式碼塊時,儘量提供程式碼的來源資訊。
開發者模式下開啟如下兩個開關,然後重啟即可。
adb shell settings put global enable_freeform_support 1
adb shell settings put global force_resizable_activities 1
# 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
<!-- add for freeform -->
<bool name="config_freeformWindowManagement">true</bool>
主要的啟動方式,一個是多工裡面,點選應用圖示,選擇小窗模式,另一個是通過三方應用啟動,比如側邊欄,通知欄等待。
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。
int startActivityFromRecents(int callingPid, int callingUid, int taskId,
SafeActivityOptions options) {
final Task task;
final int taskCallingUid;
final String callingPackage;
final String callingFeatureId;
final Intent intent;
final int userId;
final ActivityOptions activityOptions = options != null
? options.getOptions(this)
: null;
boolean moveHomeTaskForward = true;
synchronized (mService.mGlobalLock) {
int activityType = ACTIVITY_TYPE_UNDEFINED;
if (activityOptions != null) {
activityType = activityOptions.getLaunchActivityType();
final int windowingMode = activityOptions.getLaunchWindowingMode();
應用需要設定 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;
}
/dev/src/frameworks/base/core/java/android/content/pm/PackageParser.java?#L3623-L3631
在Android 系統中,視窗是應用程式介面的基本單元,用於承載和顯示應用的檢視內容。每個 Activity
都有一個主視窗,但也可以有其他視窗,如對話方塊、懸浮窗等。
在應用上層的一些元件的體現上,變化不是很大,但是 Framework
中的對於視窗的管理,迭代變化一直都比較大,可能之前有的類,新的架構下就被精簡了,所以主要掌握視窗的一些概念,這樣比較容易在新的架構之下找到對應的實現。
這裡先回顧一下基礎的視窗和介面相關的概念:
Activity
都有一個主視窗,但也可以有其他視窗,如對話方塊、懸浮窗等。View
是 Android 中使用者介面的基本構建塊,用於在視窗中繪製和顯示內容。它是視窗中可見元素的基礎。ViewGroup
是一種特殊的 View
,它可以包含其他檢視(包括 View
和其他 ViewGroup
)來形成複雜的使用者介面。Surface
是用於繪製圖形內容的區域,視窗和檢視內容都可以在 Surface
上繪製。每個視窗通常對應一個 Surface
。Surface
,以及在螢幕上繪製這些 Surface
。LayoutParams
是視窗或檢視的佈局引數,用於指定檢視在其父檢視中的位置、大小和外觀等。Window Token
是一個用於標識視窗所屬於的應用程式或任務的物件。它在視窗的顯示和互動中起著重要作用。Window Decor
是視窗的裝飾元素,如標題列、狀態列等,可以影響視窗的外觀和互動。PhoneWindow
實現了 Window.Callback 介面,該介面用於處理視窗事件和互動。通過實現這個介面,您可以監聽和響應視窗的狀態變化、輸入事件等。DecorView
承載。DecorView 是一個特殊的 ViewGroup
,用於包含應用程式的使用者介面內容和視窗裝飾元素,如標題列、狀態列等。PhoneWindow
是 android.view.Window
類的實現之一,用於表示一個應用程式視窗。它提供了視窗的基本功能,如繪製、佈局、裝飾、焦點管理等。 /** Can be freely resized within its parent container. */ /** 可以在其父容器中自由調整大小。 */
// TODO: Remove once freeform is migrated to wm-shell.
public static final int WINDOWING_MODE_FREEFORM = 5;
/** Generic multi-window with no presentation attribution from the window manager. */
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_FREEFORM,
})
public @interface WindowingMode {}
/dev/src/frameworks/base/core/java/android/app/WindowConfiguration.java?#L107-L121
WINDOWING_MODE_FULLSCREEN(全螢幕視窗模式):
WINDOWING_MODE_MULTI_WINDOW(多視窗模式):
WINDOWING_MODE_PINNED(固定視窗模式):
WINDOWING_MODE_FREEFORM(自由視窗模式):
這些視窗模式提供了不同的使用者體驗和多工處理方式,使Android裝置適應了各種不同的使用情境和裝置型別。
原生的小窗屬於 WINDOWING_MODE_FREEFORM
,對於目前大部分國內廠商而言,在WindowConfiguration
的視窗型別的基礎上,自定義一個 WINDOWING_MODE_XXX
,具體邏輯也是參考小窗的視窗型別,用於實現自己的小窗的功能。
相比原生的小窗,國內廠商實現的小窗支援的功能和動畫效果更加豐富,但是原理大致是一樣的,核心的區別就是視窗的邊界,自定義的視窗一般是通過矩陣變化中的縮放實現將應用從大螢幕到小屏,保留了原始的長寬比,這樣就不會出現變形的情況並且能夠相容所有的應用。
而原生的實現,主要是對應用邊界的控制,長寬比不固定,並且將邊界切到很小的時候,這樣會讓應用顯示不完全,並且視窗型別需要應用提前定義為 resizeableActivity
可支援縮放。
通過dumpsys window
可以看到小窗的Task
狀態,bounds
表示視窗的邊界, 一個是mode
欄位描述視窗型別.
Task display areas in top down Z order:
TaskDisplayArea DefaultTaskDisplayArea
mPreferredTopFocusableRootTask=Task{1595f4c #23 type=standard A=10130:com.youku.phone U=0 visible=true visibleRequested=true mode=freeform translucent=false sz=1}
mLastFocusedRootTask=Task{1595f4c #23 type=standard A=10130:com.youku.phone U=0 visible=true visibleRequested=true mode=freeform translucent=false sz=1}
Application tokens in top down Z order:
* Task{1595f4c #23 type=standard A=10130:com.youku.phone U=0 visible=true visibleRequested=true mode=freeform translucent=false sz=1}
bounds=[50,50][553,991]
* ActivityRecord{aec2841 u0 com.youku.phone/com.youku.v2.HomePageEntry} t23 d0}
小窗主要在 DecorCaptionView
中處理, Activity -> PhoneWindow -> DecorView -> DecorCaptionView
,相比 DecorView
多了一個 Caption(標題列)。
建立的條件有兩種:
一個在 PhoneWindow
建立的時候,建立 DecorView
,DecorView
會判斷是否要建立一個 DecorCaptionView
。
第二個就是在 DecorView
中動態變化中,有引數變數,通過onWindowSystemUiVisibilityChanged
(View的可見性變化) 以及 onConfigurationChanged
(各種觸發設定變化的條件) 的回撥,實現對DecorView
進行是否要新建一個小窗的檢視。
在 DecorView 中主要處理了小窗的引數變化,以及標題列的顯示與隱藏。
private void updateDecorCaptionStatus(Configuration config) {
final boolean displayWindowDecor = config.windowConfiguration.hasWindowDecorCaption()
&& !isFillingScreen(config); // 如果定義視窗型別為小窗,且不是全螢幕模式, 則建立一個 DecorCaptionView 在 DecorView 內部。
if (mDecorCaptionView == null && displayWindowDecor) {
// Configuration now requires a caption.
final LayoutInflater inflater = mWindow.getLayoutInflater();
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) { // 如果已經建立了 DecorCaptionView, 則更新設定資訊,比如視窗大小,視窗位置等。
// We might have to change the kind of surface before we do anything else.
mDecorCaptionView.onConfigurationChanged(displayWindowDecor);
enableCaption(displayWindowDecor); // 是否顯示小窗的標題列
}
}
/dev/src/frameworks/base/core/java/com/android/internal/policy/DecorView.java?#L2179-L2200
private View mCaption; // 標題列
private View mContent; // 小窗View之下,應用的根View
private View mMaximize; // 最大化按鈕
private View mClose; // 關閉按鈕
/dev/src/frameworks/base/core/java/com/android/internal/widget/DecorCaptionView.java?#L81-L84
如果在應用的根View外部點選的話,就攔截事件往下傳遞,這樣就不會觸發應用的點選事件。
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
int action = event.getAction();
if (mHasCaption && isShowingCaption()) { // 是否顯示標題列
// Don't dispatch ACTION_DOWN to the captionr if the window is resizable and the event
// was (starting) outside the window. Window resizing events should be handled by
// WindowManager. // 如果視窗可調整大小,並且事件是(開始)在視窗外部,則不要將 ACTION_DOWN 事件進行傳遞。視窗調整大小事件應由 WindowManager 處理。
// TODO: Investigate how to handle the outside touch in window manager
// without generating these events.
// Currently we receive these because we need to enlarge the window's
// touch region so that the monitor channel receives the events
// in the outside touch area. // TODO: 瞭解如何在視窗管理器中處理外部觸控,而不會生成這些事件。當前,我們收到這些事件,因為我們需要擴大視窗的觸控區域,以便監視通道接收外部觸控區域中的事件。
if (action == MotionEvent.ACTION_DOWN) {
final int x = (int) event.getX();
final int y = (int) event.getY();
if (isOutOfInnerBounds(x, y)) {
return true;
}
}
}
/dev/src/frameworks/base/core/java/com/android/internal/policy/DecorView.java?#L525-L544
在小窗的View中,應用就不能通過View的事件來直接處理,DecorCaptionView 需要通過計算View所在的矩形區域,然後計算點選的區域是否處於該矩形區域範圍內判斷為點選,就是一個點和麵的問題,原生小窗上的問題就是這個觸控面太小,手指的點選區域可能不容易觸發。
(在我開發的ChatDev遊戲中,也有面和麵碰撞的問題,玩家的位置和碰撞位置的計算)
事件分發的流程是這樣的:
ViewGroup::dispatchTouchEvent -> ViewGroup::onInterceptTouchEvent -> ViewGroup::onTouchEvent -> View::dispatchTouchEvent -> View::onTouchEvent
這裡 DecorCaptionView 通過onInterceptTouchEvent
攔截了事件用來實現觸控。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// If the user starts touch on the maximize/close buttons, we immediately intercept, so
// that these buttons are always clickable. // 如果使用者點選最大化或者關閉按鈕,就攔截事件,這樣就不會觸發應用的點選事件
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;
}
/dev/src/frameworks/base/core/java/com/android/internal/widget/DecorCaptionView.java?#L142-L158
目前國內的廠商的小窗設計,都是通過在DecorView
裡面模仿DecorCaptionView
自定義一個View
,用來作為小窗內部應用的容器。
常規的,圓角的實現主要是通過 View
的 setOutlineProvider
, 然後在對自定義的 DecorCaptionView
上面繪製一個圓角的背景,這樣就可以實現圓角的效果。
在 Android 中,setOutlineProvider()
是一個 View 的方法,可以用來為 View
設定一個 OutlineProvider
。OutlineProvider
是一個抽象類,它提供了獲取 View 輪廓的方法。通過設定 OutlineProvider
,可以使 View 在繪製時具有特定的輪廓形狀。
建立一個類繼承自 OutlineProvider
,並實現其 getOutline()
方法。在該方法中,可以通過呼叫 setRoundRect()、setOval() 等方法來設定不同的輪廓形狀。
通過 View.setOutlineProvider()
方法,將上一步建立的 OutlineProvider
範例設定給相應的 View。
在 View
的 onDraw()
方法中,可以利用 View.getOutlineProvider()
獲取當前設定的 OutlineProvider
範例,從而獲取 View
的輪廓,並將其繪製出來。
但是這樣的實現方式,對於在小窗內部的 View
的繪製還是不可控,所以內部的View在可繪製的區域繼續繪製矩形還是會出現顯示問題。
DisplayPolicy 作為系統裡面控制顯示 dock欄、狀態列、導航欄的樣式的主要類,
在每次繪製佈局之後,都會走到如下applyPostLayoutPolicyLw
函數,進行顯示規則的條件,當判斷重疊之後,在導航欄更新透明度規則的時候,將其標記中不透明的純深色背景和淺色前景清空。
// Check if the freeform window overlaps with the navigation bar area. // 檢查自由視窗是否與導航欄區域重疊。
if (!mIsFreeformWindowOverlappingWithNavBar && win.inFreeformWindowingMode()
&& win.mActivityRecord != null && isOverlappingWithNavBar(win)) { // 如果視窗是自由視窗,並且視窗和導航欄重疊
mIsFreeformWindowOverlappingWithNavBar = true;
}
/dev/src/frameworks/base/services/core/java/com/android/server/wm/DisplayPolicy.java?#L1585-L1591
/**
* @return the current visibility flags with the nav-bar opacity related flags toggled based
* on the nav bar opacity rules chosen by {@link #mNavBarOpacityMode}.
*/
private int configureNavBarOpacity(int appearance, boolean multiWindowTaskVisible,
boolean freeformRootTaskVisible) {
final boolean drawBackground = drawsBarBackground(mNavBarBackgroundWindow);
if (mNavBarOpacityMode == NAV_BAR_FORCE_TRANSPARENT) {
if (drawBackground) {
appearance = clearNavBarOpaqueFlag(appearance);
}
} else if (mNavBarOpacityMode == NAV_BAR_OPAQUE_WHEN_FREEFORM_OR_DOCKED) {
if (multiWindowTaskVisible || freeformRootTaskVisible) {
if (mIsFreeformWindowOverlappingWithNavBar) { // 如果自由視窗和導航欄重疊
appearance = clearNavBarOpaqueFlag(appearance); // 清除使導航欄變成不透明的純深色背景和淺色前景。
}
/dev/src/frameworks/base/services/core/java/com/android/server/wm/DisplayPolicy.java?#L2530-L2546
private void updateElevation() {
final int windowingMode =
getResources().getConfiguration().windowConfiguration.getWindowingMode();
final boolean renderShadowsInCompositor = mWindow.mRenderShadowsInCompositor;
// If rendering shadows in the compositor, don't set an elevation on the view // 如果在合成器中渲染陰影,請不要在檢視上設定高度
if (renderShadowsInCompositor) {
return;
}
float elevation = 0;
final boolean wasAdjustedForStack = mElevationAdjustedForStack;
// Do not use a shadow when we are in resizing mode (mBackdropFrameRenderer not null)
// since the shadow is bound to the content size and not the target size. // 當我們處於調整大小模式(mBackdropFrameRenderer不為空)時不要使用陰影,因為陰影繫結到內容大小而不是目標大小。
if ((windowingMode == WINDOWING_MODE_FREEFORM) && !isResizing()) {
elevation = hasWindowFocus() ?
DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP : DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP; // 如果有焦點,則為DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP=20,否則為DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP=5
// Add a maximum shadow height value to the top level view.
// Note that pinned stack doesn't have focus
// so maximum shadow height adjustment isn't needed. // 為頂級檢視新增最大陰影高度值。注意,固定堆疊沒有焦點,因此不需要最大陰影高度調整。
// TODO(skuhne): Remove this if clause once b/22668382 got fixed.
if (!mAllowUpdateElevation) {
elevation = DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP;
}
// Convert the DP elevation into physical pixels.
elevation = dipToPx(elevation);
mElevationAdjustedForStack = true;
}
/dev/src/frameworks/base/core/java/com/android/internal/policy/DecorView.java?#L2530-L2555
小窗的背景中,包含應用對View設定的背景資源 還有一個視窗容器外部存在一個調暗層(DimLayer
)的背景,用於實現調暗的效果。
Dimmer 類的作用是為實現視窗容器(WindowContainer
)新增「調暗層」(DimLayer)支援,通過在不同的 Z 層級上建立具有不同透明度的黑色層,從而實現調暗的效果。該類主要用於在視窗容器中管理和應用調暗效果。
該類的主要方法和功能如下:
dimAbove
和 dimBelow
方法
這些方法用於在指定的視窗容器上方或下方新增調暗層。可以設定調暗層的透明度(alpha)和相對於指定容器的 Z 層級。
resetDimStates
該方法標記所有的調暗狀態為等待下一次呼叫 updateDims 時完成。在呼叫子容器的 prepareSurfaces 之前呼叫此方法,以允許子容器繼續請求保持調暗。
updateDims 方法
在呼叫子容器的 prepareSurfaces 後,通過此方法來更新調暗層的位置和尺寸。根據容器的更新,它可以設定調暗層的位置、大小以及調整動畫。
stopDim 方法
用於停止調暗效果,可以在不再需要調暗時呼叫。
內部類 DimState
表示調暗狀態的內部類,包含調暗層的 SurfaceControl、調暗狀態等資訊。
Dimmer
類用於管理和應用調暗效果,使視窗容器在不同 Z 層級上新增透明度變化的調暗層,以達到調暗的效果。
在 Android 系統中用於處理視窗切換、過渡動畫以及調整顯示效果時很有用, 如果要對小窗進行圓角的處理,這一層也是要處理的, 不然會出現黑色矩形邊的問題。
startSpecificActivity 方法的作用是啟動指定的活動(Activity)範例。
這個方法通常用於特定的情況,例如當需要在特定任務中啟動某個活動時,或者在特定的任務堆疊中啟動活動。
具體來說,startSpecificActivity 方法的作用包括:
啟動指定活動: 該方法允許開發者或系統通過提供特定的活動元件資訊(如包名、類名)來啟動特定的活動。
指定任務和堆疊: 該方法可以允許開發者或系統指定在哪個任務和堆疊中啟動活動。這有助於將活動放置在預期的上下文中,如在多視窗模式中。
工作切換和前臺切換: 在啟動指定活動時,系統可能需要調整任務堆疊和前臺任務。這可以用於在使用者點選通知或從其他應用程式啟動時,確保正確的工作切換和前臺切換。
void startSpecificActivity(ActivityRecord r, boolean andResume, boolean checkConfig) {
// Is this activity's application already running?
final WindowProcessController wpc =
mService.getProcessController(r.processName, r.info.applicationInfo.uid);
boolean knownToBeDead = false;
if (wpc != null && wpc.hasThread()) {
try {
realStartActivityLocked(r, wpc, andResume, checkConfig);
return;
} catch (RemoteException e) {
Slog.w(TAG, "Exception when starting activity "
+ r.intent.getComponent().flattenToShortString(), e);
}
// If a dead object exception was thrown -- fall through to
// restart the application.
knownToBeDead = true;
// Remove the process record so it won't be considered as alive.
mService.mProcessNames.remove(wpc.mName, wpc.mUid);
mService.mProcessMap.remove(wpc.getPid());
}
r.notifyUnknownVisibilityLaunchedForKeyguardTransition();
final boolean isTop = andResume && r.isTopRunningActivity();
mService.startProcessAsync(r, knownToBeDead, isTop,
isTop ? HostingRecord.HOSTING_TYPE_TOP_ACTIVITY
: HostingRecord.HOSTING_TYPE_ACTIVITY);
}
resumeTopActivityUncheckedLocked 是 Android 系統中 ActivityStack 類中的一個重要函數,它用於在堆疊中恢復位於頂部的活動(Activity)。這個函數主要用於活動的切換和前臺任務的管理。
具體來說,resumeTopActivityUncheckedLocked 的作用包括:
活動恢復: 當一個活動需要從後臺切換到前臺時,這個函數負責執行恢復操作。它會執行活動的生命週期方法,如 onResume,以確保活動處於可見和活躍狀態。
前臺工作切換: 當用戶切換到另一個任務時,或者當一個任務從後臺切換到前臺時,這個函數負責在不同的任務堆疊之間切換活動。它確保目標活動所在的任務堆疊處於前臺,以便使用者能夠與其互動。
工作切換的恢復: 當前臺任務從另一個工作切換回來時,系統需要確保前臺任務的活動被正確恢復。這個函數負責在這種情況下的活動切換和恢復。
多視窗模式支援: 如果裝置支援多視窗模式,這個函數也會在多視窗切換時被呼叫,以確保活動在不同視窗模式之間正確切換和恢復。
在Android Framework中,WindowState 是用於表示視窗狀態的一個類。
視窗狀態是指應用程式視窗在螢幕上的顯示狀態,包括位置、大小、可見性等。
computeFrame方法是WindowState類中的一個重要方法,用於計算視窗的位置和大小。具體來說,它負責計算視窗的繪製區域,即視窗的內容在螢幕上實際顯示的位置和大小。這個計算涉及到考慮視窗的位置、大小、佈局引數以及可能的邊界限制,確保視窗內容不會超出螢幕邊界或被其他視窗遮擋。
在視窗管理器中,computeFrame方法通常會在以下情況被呼叫:
當視窗第一次被建立時,需要計算其初始位置和大小。
當視窗的佈局引數或內容發生變化時,需要重新計算視窗的位置和大小。
當螢幕旋轉或大小變化等系統事件發生時,需要調整所有視窗的位置和大小。
總之,computeFrame方法在Android視窗管理系統中起到了非常重要的作用,確保應用程式視窗能夠正確地在螢幕上顯示,並且適應不同的裝置和系統事件, 為了計算小窗的位置,以及處理小窗內的View 的邊界異常情況,
通常我們需要對 WindowFrames 是一個表示視窗邊框大小和位置的類進行適當的處理。
其中,mFrame、mVisibleFrame、mDecorFrame、mDisplayFrame 是 WindowFrames 中的一些成員變數,用於描述不同的視窗區域。
這些成員變數共同描述了視窗在螢幕中的位置和大小,並提供給其他模組使用,比如 WindowManager 和 View 系統。
在 Android Framework 中,WindowManagerService 會在每次視窗大小發生變化時,呼叫 WindowFrames 的 setFrames() 方法,更新這些成員變數的值。
在 Task
類中有一個比較重要的函數,prepareSurfaces
, 通常是在視窗或介面元素繪製之前被呼叫的一個方法,用於準備和更新與繪製相關的表面(Surface)的狀態。
和視窗相關的系統類,這裡梳理一下需要了解的關鍵繼承關係:
- WindowContainer::prepareSurfaces
- public class DisplayArea<T extends WindowContainer> extends WindowContainer<T> {
- class RootWindowContainer extends WindowContainer<DisplayContent>
- class TaskFragment extends WindowContainer<WindowContainer> {
- class Task extends TaskFragment {
- class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<E>
- class WindowState extends WindowContainer<WindowState> implements WindowManagerPolicy.WindowState,
- class WindowToken extends WindowContainer<WindowState> {
prepareSurfaceLocked:472, WindowStateAnimator (com.android.server.wm)
prepareSurfaces:5653, WindowState (com.android.server.wm)
prepareSurfaces:2669, WindowContainer (com.android.server.wm)
prepareSurfaces:7327, ActivityRecord (com.android.server.wm)
prepareSurfaces:2669, WindowContainer (com.android.server.wm)
prepareSurfaces:2639, TaskFragment (com.android.server.wm)
prepareSurfaces:3323, Task (com.android.server.wm)
prepareSurfaces:2669, WindowContainer (com.android.server.wm)
prepareSurfaces:2669, WindowContainer (com.android.server.wm)
prepareSurfaces:2669, WindowContainer (com.android.server.wm)
prepareSurfaces:2669, WindowContainer (com.android.server.wm)
prepareSurfaces:2669, WindowContainer (com.android.server.wm)
prepareSurfaces:662, DisplayArea$Dimmable (com.android.server.wm)
prepareSurfaces:2669, WindowContainer (com.android.server.wm)
prepareSurfaces:662, DisplayArea$Dimmable (com.android.server.wm)
prepareSurfaces:5242, DisplayContent (com.android.server.wm)
applySurfaceChangesTransaction:4717, DisplayContent (com.android.server.wm)
applySurfaceChangesTransaction:1025, RootWindowContainer (com.android.server.wm)
performSurfacePlacementNoTrace:828, RootWindowContainer (com.android.server.wm)
performSurfacePlacement:788, RootWindowContainer (com.android.server.wm)
performSurfacePlacementLoop:177, WindowSurfacePlacer (com.android.server.wm)
performSurfacePlacement:126, WindowSurfacePlacer (com.android.server.wm)
performSurfacePlacement:115, WindowSurfacePlacer (com.android.server.wm)
handleMessage:5680, WindowManagerService$H (com.android.server.wm)
dispatchMessage:106, Handler (android.os)
loopOnce:201, Looper (android.os)
loop:288, Looper (android.os)
run:67, HandlerThread (android.os)
run:44, ServiceThread (com.android.server)
@Override
void prepareSurfaces() {
mDimmer.resetDimStates();
super.prepareSurfaces();
getDimBounds(mTmpDimBoundsRect);
// Bounds need to be relative, as the dim layer is a child. // 邊界需要是相對的,因為暗層是子層。
if (inFreeformWindowingMode()) {
getBounds(mTmpRect);
mTmpDimBoundsRect.offsetTo(mTmpDimBoundsRect.left - mTmpRect.left,
mTmpDimBoundsRect.top - mTmpRect.top); // 處理調暗層的偏移邊界
} else {
mTmpDimBoundsRect.offsetTo(0, 0);
}
/dev/src/frameworks/base/services/core/java/com/android/server/wm/Task.java?#L3306-L3319
prepareSurfaces
在圖形渲染和顯示過程中發揮著重要作用,確保繪製的介面元素能夠正確顯示和互動。
正常有這些狀態下會呼叫 prepareSurfaces
函數:
Surface 的建立和設定: 在介面元素即將繪製之前,prepareSurfaces 函數可能會建立、設定或更新相關的 Surface。這可能包括在繪製過程中使用的後備 Surface 或用於渲染特定檢視的 Surface。
更新介面狀態: 該函數通常用於更新介面元素的狀態,如位置、大小、可見性等。這可以確保介面元素在繪製之前具有正確的狀態。
表面層級設定: prepareSurfaces 通常會設定不同 Surface 之間的層級關係,以確保它們按照正確的順序繪製。這對於實現疊加效果、混合效果和視窗層次等非常重要。
調暗效果的管理: 在一些情況下,prepareSurfaces 可能會用於管理調暗效果,如前一個問題中提到的 Dimmer 類。在繪製前調整表面的透明度和位置,以達到調暗的效果。
動畫和過渡準備: 如果有動畫或過渡效果,prepareSurfaces 可能會在繪製之前對錶面進行適當的準備,以確保動畫效果的正確執行。
效能優化: prepareSurfaces 也可以用於效能優化,例如準備繪製所需的材料、紋理或緩衝區,以避免在實際繪製時出現延遲。
DisplayContent
類的一個方法,名為 processTaskForTouchExcludeRegion
。
處理任務的觸控排除區域,以確保在處理觸控事件時不會影響到特定區域,例如任務之間的間隙或任務的邊界。以下是對程式碼的逐行分析和大致功能的解釋:
private void processTaskForTouchExcludeRegion(Task task, Task focusedTask, int delta) {
final ActivityRecord topVisibleActivity = task.getTopVisibleActivity();
if (topVisibleActivity == null || !topVisibleActivity.hasContentToDisplay()) { // 檢查頂部可見活動是否存在,以及該活動是否有內容需要顯示。如果沒有,就跳過後續處理。
return;
}
// Exclusion region is the region that TapDetector doesn't care about.
// Here we want to remove all non-focused tasks from the exclusion region.
// We also remove the outside touch area for resizing for all freeform
// tasks (including the focused).
// We save the focused task region once we find it, and add it back at the end.
// If the task is root home task and it is resizable and visible (top of its root task),
// we want to exclude the root docked task from touch so we need the entire screen area
// and not just a small portion which the root home task currently is resized to. // 如果任務是根主頁任務,並且它是可調整大小的並且可見的(在其根任務的頂部),我們希望從觸控中排除根停靠任務,因此我們需要整個螢幕區域而不僅僅是根主頁任務當前調整大小的一小部分。
if (task.isActivityTypeHome() && task.isVisible() && task.isResizeable()) {
task.getDisplayArea().getBounds(mTmpRect);
} else {
task.getDimBounds(mTmpRect); // 獲取任務的調整邊界(維度邊界)
}
if (task == focusedTask) {
// Add the focused task rect back into the exclude region once we are done
// processing root tasks.
// NOTE: this *looks* like a no-op, but this usage of mTmpRect2 is expected by
// updateTouchExcludeRegion.
mTmpRect2.set(mTmpRect); // 將當前任務的邊界複製到臨時矩形 mTmpRect2, 用於後續的更新操作。
}
final boolean isFreeformed = task.inFreeformWindowingMode();
if (task != focusedTask || isFreeformed) {
if (isFreeformed) {
// If the task is freeformed, enlarge the area to account for outside
// touch area for resize. // 如果任務是小窗模式,則擴大該區域以考慮調整大小的外部觸控區域。
mTmpRect.inset(-delta, -delta);
// Intersect with display content frame. If we have system decor (status bar/
// navigation bar), we want to exclude that from the tap detection.
// Otherwise, if the app is partially placed under some system button (eg.
// Recents, Home), pressing that button would cause a full series of
// unwanted transfer focus/resume/pause, before we could go home.
mTmpRect.inset(getInsetsStateController().getRawInsetsState().calculateInsets(
mTmpRect, systemBars() | ime(), false /* ignoreVisibility */)); // 調整任務邊界,以排除系統裝飾(如狀態列、導航欄)和輸入法區域的影響。
}
mTouchExcludeRegion.op(mTmpRect, Region.Op.DIFFERENCE); // 將調整後的任務邊界與觸控排除區域進行排除操作,以確保觸控事件不會影響到此區域。
}
}
/dev/src/frameworks/base/services/core/java/com/android/server/wm/DisplayContent.java?#L3167-L3212
藉助 Android 拖放框架,您可以嚮應用中新增互動式拖放功能。通過拖放,使用者可以在應用中的 View 之間複製或拖動文字、圖片、物件(可以通過 URI 表示的任何內容),也可以使用多視窗模式在應用之間拖動這些內容。
https://developer.android.google.cn/guide/topics/ui/drag-drop?hl=en
DragState
類主要用於管理和跟蹤拖拽操作的狀態。拖拽操作通常涉及使用者在螢幕上拖動某個檢視或物件,並在特定位置釋放它。以下是 DragState
類的主要作用和功能:
管理拖拽動作的狀態:DragState
類負責跟蹤拖拽操作的各個狀態,例如開始拖拽、拖拽中、拖拽結束等。它可以儲存和更新與拖拽狀態相關的資訊。
處理拖拽手勢:這個類可能包含了處理使用者拖拽手勢的邏輯,例如監測使用者手指的移動、計算拖拽物體的位置、響應使用者的拖拽操作等。
協調拖拽操作:DragState
類可能與其他系統元件(如視窗管理器或檢視系統)協同工作,以確保拖拽操作在螢幕上正確執行。它可能需要調整被拖拽物體的位置,更新UI,或者觸發其他操作。
提供拖拽狀態資訊:這個類通常會提供有關拖拽狀態的資訊,例如拖拽物體的位置、拖拽的型別、拖拽的源物件等。這些資訊可以供其他元件使用。
處理拖拽的釋放:當用戶釋放拖拽物體時,DragState
類可能需要執行特定的操作,例如將拖拽物體放置在新的位置、觸發操作、或者完成拖拽操作。
支援拖拽的視覺化效果:在一些情況下,DragState
類可能需要管理與拖拽相關的視覺化效果,例如拖拽物體的影子或者拖拽物體的預覽。
DragState
notifyLocationLocked(float, float)
WindowState touchedWin = mDisplayContent.getTouchableWinAtPointLocked(x, y);
notifyDropLocked(float, float)
final WindowState touchedWin = mDisplayContent.getTouchableWinAtPointLocked(x, y);
WindowManagerService
updatePointerIcon(IWindow)
displayContent.getTouchableWinAtPointLocked(mouseX, mouseY);
restorePointerIconLocked(DisplayContent, float, float)
displayContent.getTouchableWinAtPointLocked(latestX, latestY);
在小窗模式下,可以通過長按小窗的標題列,然後拖動到螢幕的任意位置,這個過程中會呼叫 moveTaskToStack
,具體實現如下:
目前從 Android12 開始,Android Framework逐步將Stack
的概念切換為Task
了,比如之前的 ActivityStackSupervisor
變成了 ActivityTaskSupervisor
, ActivitStack
也變成了 Task
,
在之前的版本上,也兩個概念也同步存在一段時間。
ActivityTaskManagerService 是系統中負責管理應用程式的任務(Task)和活動(Activity)。
它在Android系統中起到了排程和管理應用程式的核心角色。 主要作用有:
任務管理: 該服務負責管理多個應用程式任務的生命週期。任務是一組相關活動的集合,通常代表了一個使用者在應用程式之間的導航路徑。例如,使用者在瀏覽器中開啟一個網頁,然後從瀏覽器跳轉到電子郵件應用,這兩個活動會被歸為一個任務。工作管理員確保任務在切換活動或返回到最近使用的任務時得以恢復。
活動管理: 該服務負責管理單個活動的生命週期。活動是應用程式使用者介面的基本單元,包含使用者與應用程式互動的介面元素。ActivityTaskManagerService
確保活動在需要時正確建立、暫停、恢復、銷燬等。
工作切換和前臺應用管理: 當用戶切換應用程式或任務時,ActivityTaskManagerService
協調活動之間的切換,以及確保前臺應用程式的正確管理。前臺應用是當前使用者正在與之互動的應用程式,而後臺應用則是在後臺執行的應用程式。
多視窗支援: Android系統中的一些裝置支援多視窗模式,允許使用者在同一螢幕上同時執行多個應用程式。ActivityTaskManagerService
負責管理多視窗模式下的任務和活動排列。
任務堆疊管理: ActivityTaskManagerService
管理應用程式任務的堆疊,以便在不同任務之間進行切換。這確保使用者能夠流暢地在不同應用程式之間切換,並且系統能夠適當地處理返回鍵等使用者導航操作。
它用來確保應用程式任務和活動的正確管理、切換和互動,從而提供了良好的使用者體驗。
ActivityTaskSupervisor 在 ActivityTaskManagerService 共同管理的應用程式任務和活動,它是 ActivityManagerService 的一部分,用於協調和管理任務的生命週期、工作切換、多視窗模式等方面的功能。
// TODO: This class has become a dumping ground. Let's
// - Move things relating to the hierarchy to RootWindowContainer
// - Move things relating to activity life cycles to maybe a new class called ActivityLifeCycler
// - Move interface things to ActivityTaskManagerService.
// - All other little things to other files.
public class ActivityTaskSupervisor implements RecentTasks.Callbacks {
在新的結構中,將一些功能從 ActivityTaskSupervisor 中移除,分別放到 ActivityTaskManagerService
、RootWindowContainer
和 ActivityLifeCycler
中,使得 ActivityTaskSupervisor 只保留一些核心功能,從而使得程式碼更加清晰。
小窗右上角點選切換全螢幕應用,會從小窗的模式變成應用全螢幕的模式,切換的邏輯從 DecorCaptionView
中 呼叫toggleFreeformWindowingMode
,在Android13之前通過 ActivityTaskManagerService 來實現,在
Android12之後新增了一個系統類ActivityClientController
(在伺服器端實現,用於使用者端活動與系統互動,避免了之前在應用層呼叫系統服務的時候,之前直接呼叫的使用者端需要自行處理RemoteException
以及其他多餘的邏輯,這裡增加了一層封裝,提高了效率),在ActivityClientController中新增了一個方法toggleFreeformWindowingMode
,用於切換小窗和全螢幕的模式,具體實現如下:
@Override
public void toggleFreeformWindowingMode(IBinder token) { //
final long ident = Binder.clearCallingIdentity();
try {
synchronized (mGlobalLock) {
final ActivityRecord r = ActivityRecord.forTokenLocked(token);
if (r == null) {
throw new IllegalArgumentException(
"toggleFreeformWindowingMode: No activity record matching token="
+ token);
}
final Task rootTask = r.getRootTask();
if (rootTask == null) {
throw new IllegalStateException("toggleFreeformWindowingMode: the activity "
+ "doesn't have a root task");
}
if (!rootTask.inFreeformWindowingMode()
&& rootTask.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) {
throw new IllegalStateException("toggleFreeformWindowingMode: You can only "
+ "toggle between fullscreen and freeform.");
}
if (rootTask.inFreeformWindowingMode()) {
rootTask.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
rootTask.setBounds(null);
} else if (!r.supportsFreeform()) {
這裡有幾個變化,一個是以前通過setWindowingMode
切換視窗處理的物件是ActivityStack
而 ActivityStack
繼承自Task
, 在Android13上, 直接把ActivityStack
這個類幹掉了,處理的視窗型別物件為 Task
,
在一個是新增了 setBounds(null)
.
setWindowingMode
手動更新視窗型別,主要是為了根節點設計,如果是子節點來呼叫,需要找到父節點,然後設定它的視窗型別。
@Override
public void setWindowingMode(int windowingMode) {
// Calling Task#setWindowingMode() for leaf task since this is the a specialization of
// {@link #setWindowingMode(int)} for root task.
if (!isRootTask()) {
super.setWindowingMode(windowingMode);
return;
}
setWindowingMode(windowingMode, false /* creating */);
}
/dev/src/frameworks/base/services/core/java/com/android/server/wm/Task.java?#L4531-L4541
ActivityRecord 是一個資料結構,用於表示應用程式中的活動(Activity)範例。
每個正在執行或已經啟動但尚未銷燬的活動都會有一個對應的 ActivityRecord 物件來跟蹤其狀態和資訊。
ActivityRecord通常由 ActivityTaskSupervisor 等元件使用,用於管理和控制活動的生命週期和互動。
狀態跟蹤: ActivityRecord用於跟蹤活動的當前狀態,包括執行狀態、暫停狀態、停止狀態等。它記錄活動是否已經建立、啟動、恢復,以及是否已經被銷燬。
任務和堆疊資訊: 每個ActivityRecord通常都屬於某個任務(Task),幷包含有關其所在任務的資訊,例如任務的ID、堆疊的ID等。這有助於在任務和堆疊之間正確切換活動。
視窗管理: ActivityRecord提供與視窗管理相關的資訊,如視窗的位置、大小、可見性等。這些資訊用於將活動的UI內容正確地顯示在螢幕上。
意圖和引數: 通過ActivityRecord,可以存取啟動活動時傳遞的意圖(Intent)和附加引數。這對於恢復活動狀態、處理意圖資料等操作很有幫助。
生命週期管理: ActivityRecord記錄了活動的生命週期事件,如建立、啟動、恢復、暫停、停止和銷燬等。這有助於ActivityStackSupervisor等元件正確管理活動的生命週期。
在 ActivityTaskSupervisor 中,記錄了一些活動的狀態資訊,如下所示:
/** List of activities that are ready to be stopped, but waiting for the next activity to
* settle down before doing so. */ // 列表中的活動已經準備好停止,但是等待下一個活動安定下來才能停止。
final ArrayList<ActivityRecord> mStoppingActivities = new ArrayList<>();
/** List of activities that are ready to be finished, but waiting for the previous activity to
* settle down before doing so. It contains ActivityRecord objects. */ // 列表中的活動已經準備好完成,但是等待上一個活動穩定後才能完成。
final ArrayList<ActivityRecord> mFinishingActivities = new ArrayList<>();
/**
* Activities that specify No History must be removed once the user navigates away from them.
* If the device goes to sleep with such an activity in the paused state then we save it
* here and finish it later if another activity replaces it on wakeup. // 指定無歷史記錄的活動必須在使用者導航離開它們後刪除。
*/
final ArrayList<ActivityRecord> mNoHistoryActivities = new ArrayList<>();
/** List of activities whose multi-window mode changed that we need to report to the
* application */ // 列表中的活動的多視窗模式已更改,我們需要嚮應用程式報告
private final ArrayList<ActivityRecord> mMultiWindowModeChangedActivities = new ArrayList<>();
/** List of activities whose picture-in-picture mode changed that we need to report to the
* application */ // 列表中的活動的畫中畫模式已更改,我們需要嚮應用程式報告
private final ArrayList<ActivityRecord> mPipModeChangedActivities = new ArrayList<>();
/**
* Animations that for the current transition have requested not to
* be considered for the transition animation.
*/ // 動畫,對於當前轉換,已請求不考慮轉換動畫。
final ArrayList<ActivityRecord> mNoAnimActivities = new ArrayList<>();
/**
* Cached value of the topmost resumed activity in the system. Updated when new activity is
* resumed.
*/ // 系統中最頂層的恢復活動的快取值。當恢復新活動時更新。
private ActivityRecord mTopResumedActivity;
以 mStoppingActivities 為例:
ActivityRecord 類的 finishIfPossible
方法通常在以下情況下被呼叫:
清理 ActivityRecord 時 會將範例暫存到 mStoppingActivities
然後後續的範例執行穩定後,在實際去清理,這樣可以讓下一個範例啟動時的繪製效能更高,提高體驗。
private static native void nativeSetMatrix(long transactionObj, long nativeObject,
float dsdx, float dtdx,
float dtdy, float dsdy);
transactionObj
(型別:long):這是一個 SurfaceControl.Transaction
物件的本地表示。它用於指示執行此操作的事務物件。這個引數通常用來確保操作是原子的,以便它們可以一起執行或一起失敗。
nativeObject
(型別:long):這是 SurfaceControl
物件的本地表示。它是要應用變換矩陣的目標圖形表面的本地參照。
本文只是從軟體開發的角度,簡單梳理一下Android FreeForm(自由視窗) 涉及到的模組以及基本概念,本文也在持續的更新中,如果你需要得到最新的更新或者有一些程式碼高亮,請存取: Android13深入瞭解 Android 小視窗模式和視窗型別