Android14---SystemUI分析(代码、图文详细讲解)
A ---------->SystemUI布局分析1、锁屏页面2、QSB and 通知区域3、QSB状态栏4、桌面状态栏5、QSB编辑页面B ---------->SystemUI启动流程分析C ---------->SystemUI屏幕亮度调节代码分析D ---------->SystemUI之QS面板分析1、QS面板构成元素分析2、QS面板内部实现梳理a:QS面板开关集合构建流程b:Tile后
Android14---SystemUI分析(代码、图文详细讲解)
A ---------->SystemUI布局分析
SystemUI 是 Android 操作系统中的一个系统应用程序,它提供了系统用户界面的核心部分,包括状态栏、通知栏、快速设置面板、锁屏界面等(导航栏)。
状态栏通常位于屏幕顶部,显示时间、电池电量、信号强度、Wi-Fi 连接状态等信息。
通知栏是状态栏的一部分,可以下拉以显示更多的应用通知和快速设置选项。
快速设置面板通常通过下拉通知栏来访问,它提供了一系列的快捷开关和设置选项,允许用户快速调整设备的设置,如Wi-Fi、蓝牙、飞行模式、亮度调节、声音设置等。该面板的设计旨在提供快速访问常用设置,而无需打开完整的设置应用。
锁屏页面是设备在一段时间不活动后进入的一种状态,它显示时间和日期,并可能显示未读通知的摘要。
1、锁屏页面
status_bar_expanded.xml
<com.android.systemui.shade.NotificationPanelView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:systemui="http://schemas.android.com/apk/res-auto"
android:id="@+id/notification_panel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent">
<com.android.systemui.common.ui.view.LongPressHandlingView
android:id="@+id/keyguard_long_press"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ViewStub
android:id="@+id/keyguard_qs_user_switch_stub"
android:layout="@layout/keyguard_qs_user_switch"
android:layout_height="match_parent"
android:layout_width="match_parent" />
<include layout="@layout/status_bar_expanded_plugin_frame"/>
<com.android.systemui.shade.NotificationsQuickSettingsContainer
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="@integer/notification_panel_layout_gravity"
android:id="@+id/notification_container_parent"
android:clipToPadding="false"
android:clipChildren="false">
<include
layout="@layout/keyguard_status_view"
android:visibility="gone"/>
<include layout="@layout/dock_info_overlay"/>
<FrameLayout
android:id="@+id/qs_frame"
android:layout="@layout/qs_panel"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:clipChildren="false"
android:layout_marginHorizontal="@dimen/notification_panel_margin_horizontal"
systemui:viewType="com.android.systemui.plugins.qs.QS"
systemui:layout_constraintStart_toStartOf="parent"
systemui:layout_constraintEnd_toEndOf="parent"
systemui:layout_constraintTop_toTopOf="parent"
systemui:layout_constraintBottom_toBottomOf="parent"
/>
<!-- This view should be after qs_frame so touches are dispatched first to it. That gives
it a chance to capture clicks before the NonInterceptingScrollView disallows all
intercepts -->
<ViewStub
android:id="@+id/qs_header_stub"
android:layout_height="wrap_content"
android:layout_width="match_parent"
/>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/qs_edge_guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
systemui:layout_constraintGuide_percent="0.5"
android:orientation="vertical"/>
<!-- This layout should always include a version of
NotificationStackScrollLayout, as it is expected from
NotificationPanelViewController. -->
<include layout="@layout/notification_stack_scroll_layout" />
<include layout="@layout/photo_preview_overlay" />
<include
layout="@layout/keyguard_status_bar"
android:visibility="invisible" />
<Button
android:id="@+id/report_rejected_touch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/status_bar_header_height_keyguard"
android:text="@string/report_rejected_touch"
android:visibility="gone" />
<com.android.systemui.statusbar.phone.TapAgainView
android:id="@+id/shade_falsing_tap_again"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
systemui:layout_constraintLeft_toLeftOf="parent"
systemui:layout_constraintRight_toRightOf="parent"
systemui:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="20dp"
android:paddingHorizontal="16dp"
android:minHeight="44dp"
android:elevation="4dp"
android:background="@drawable/rounded_bg_full"
android:gravity="center"
android:text="@string/m_tap_again"
android:visibility="gone"
/>
</com.android.systemui.shade.NotificationsQuickSettingsContainer>
<ViewStub
android:id="@+id/keyguard_user_switcher_stub"
android:layout="@layout/keyguard_user_switcher"
android:layout_height="match_parent"
android:layout_width="match_parent" />
<include layout="@layout/dock_info_bottom_area_overlay" />
<com.android.keyguard.LockIconView
android:id="@+id/lock_icon_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<!-- Background protection -->
<ImageView
android:id="@+id/lock_icon_bg"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/fingerprint_bg"
android:visibility="invisible"/>
<ImageView
android:id="@+id/lock_icon"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:scaleType="centerCrop"/>
</com.android.keyguard.LockIconView>
<include
layout="@layout/keyguard_bottom_area"
android:visibility="gone" />
</com.android.systemui.shade.NotificationPanelView>
keyguard_status_bar.xml
<!-- Extends RelativeLayout -->
<com.android.systemui.statusbar.phone.KeyguardStatusBarView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:systemui="http://schemas.android.com/apk/res-auto"
android:id="@+id/keyguard_header"
android:layout_width="match_parent"
android:layout_height="@dimen/status_bar_header_height_keyguard"
android:baselineAligned="false"
android:gravity="center_vertical"
>
<LinearLayout
android:id="@+id/status_icon_area"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/system_icons_super_container_margin_start"
android:paddingTop="@dimen/status_bar_padding_top"
android:layout_alignParentEnd="true"
android:gravity="center_vertical|end" >
<include
android:id="@+id/user_switcher_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/status_bar_user_chip_end_margin"
layout="@layout/status_bar_user_chip_container" />
<FrameLayout android:id="@+id/system_icons_container"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginEnd="@dimen/status_bar_padding_end"
android:gravity="center_vertical|end">
<include layout="@layout/system_icons" />
</FrameLayout>
<ImageView android:id="@+id/multi_user_avatar"
android:layout_width="@dimen/multi_user_avatar_keyguard_size"
android:layout_height="@dimen/multi_user_avatar_keyguard_size"
android:layout_gravity="center"
android:scaleType="centerInside"/>
</LinearLayout>
<Space
android:id="@+id/cutout_space_view"
android:layout_width="0dp"
android:layout_height="match_parent"
android:gravity="center"
android:visibility="gone" />
<com.android.keyguard.CarrierText
android:id="@+id/keyguard_carrier_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="@dimen/status_bar_padding_top"
android:layout_marginStart="@dimen/keyguard_carrier_text_margin"
android:layout_toStartOf="@id/system_icons_container"
android:gravity="center_vertical"
android:ellipsize="marquee"
android:textDirection="locale"
android:textAppearance="@style/TextAppearance.StatusBar.Clock"
android:textColor="?attr/wallpaperTextColorSecondary"
android:singleLine="true"
systemui:showMissingSim="true"
systemui:showAirplaneMode="true"
systemui:debugLocation="Keyguard" />
</com.android.systemui.statusbar.phone.KeyguardStatusBarView>
keyguard_pattern_view.xml
<!-- This is the screen that shows the 9 circle unlock widget and instructs
the user how to unlock their device, or make an emergency call. This
is the portrait layout. -->
<com.android.keyguard.KeyguardPatternView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/res-auto"
android:id="@+id/keyguard_pattern_view"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
androidprv:layout_maxWidth="@dimen/biometric_auth_pattern_view_max_size"
android:layout_gravity="center_horizontal|bottom"
android:clipChildren="false"
android:clipToPadding="false">
<include layout="@layout/keyguard_bouncer_message_area"/>
<com.android.systemui.keyguard.bouncer.ui.BouncerMessageView
android:id="@+id/bouncer_message_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/pattern_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="8dp"
android:layout_weight="1"
android:layoutDirection="ltr">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/pattern_top_guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
androidprv:layout_constraintGuide_percent="0"
android:orientation="horizontal" />
<com.android.internal.widget.LockPatternView
android:id="@+id/lockPatternView"
android:layout_width="0dp"
android:layout_height="0dp"
androidprv:layout_constraintTop_toBottomOf="@id/pattern_top_guideline"
androidprv:layout_constraintBottom_toBottomOf="parent"
androidprv:layout_constraintLeft_toLeftOf="parent"
androidprv:layout_constraintRight_toRightOf="parent"
androidprv:layout_constraintDimensionRatio="1.0"
androidprv:layout_constraintVertical_bias="1.0"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
<include layout="@layout/keyguard_eca"
android:id="@+id/keyguard_selector_fade_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_gravity="bottom|center_horizontal"
android:layout_marginTop="@dimen/keyguard_eca_top_margin"
android:layout_marginBottom="@dimen/keyguard_eca_bottom_margin"
android:gravity="center_horizontal" />
</com.android.keyguard.KeyguardPatternView>
2、QSB and 通知区域
status_bar_expanded.xml
<com.android.systemui.shade.NotificationPanelView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:systemui="http://schemas.android.com/apk/res-auto"
android:id="@+id/notification_panel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/transparent">
<com.android.systemui.common.ui.view.LongPressHandlingView
android:id="@+id/keyguard_long_press"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ViewStub
android:id="@+id/keyguard_qs_user_switch_stub"
android:layout="@layout/keyguard_qs_user_switch"
android:layout_height="match_parent"
android:layout_width="match_parent" />
<include layout="@layout/status_bar_expanded_plugin_frame"/>
<com.android.systemui.shade.NotificationsQuickSettingsContainer
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="@integer/notification_panel_layout_gravity"
android:id="@+id/notification_container_parent"
android:clipToPadding="false"
android:clipChildren="false">
<include
layout="@layout/keyguard_status_view"
android:visibility="gone"/>
<include layout="@layout/dock_info_overlay"/>
<FrameLayout
android:id="@+id/qs_frame"
android:layout="@layout/qs_panel"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:clipChildren="false"
android:layout_marginHorizontal="@dimen/notification_panel_margin_horizontal"
systemui:viewType="com.android.systemui.plugins.qs.QS"
systemui:layout_constraintStart_toStartOf="parent"
systemui:layout_constraintEnd_toEndOf="parent"
systemui:layout_constraintTop_toTopOf="parent"
systemui:layout_constraintBottom_toBottomOf="parent"
/>
<!-- This view should be after qs_frame so touches are dispatched first to it. That gives
it a chance to capture clicks before the NonInterceptingScrollView disallows all
intercepts -->
<ViewStub
android:id="@+id/qs_header_stub"
android:layout_height="wrap_content"
android:layout_width="match_parent"
/>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/qs_edge_guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
systemui:layout_constraintGuide_percent="0.5"
android:orientation="vertical"/>
<!-- This layout should always include a version of
NotificationStackScrollLayout, as it is expected from
NotificationPanelViewController. -->
<include layout="@layout/notification_stack_scroll_layout" />
<include layout="@layout/photo_preview_overlay" />
<include
layout="@layout/keyguard_status_bar"
android:visibility="invisible" />
<Button
android:id="@+id/report_rejected_touch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/status_bar_header_height_keyguard"
android:text="@string/report_rejected_touch"
android:visibility="gone" />
<com.android.systemui.statusbar.phone.TapAgainView
android:id="@+id/shade_falsing_tap_again"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
systemui:layout_constraintLeft_toLeftOf="parent"
systemui:layout_constraintRight_toRightOf="parent"
systemui:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="20dp"
android:paddingHorizontal="16dp"
android:minHeight="44dp"
android:elevation="4dp"
android:background="@drawable/rounded_bg_full"
android:gravity="center"
android:text="@string/m_tap_again"
android:visibility="gone"
/><!-- luzongrui change for Tap again bug 73547 20240424-->
</com.android.systemui.shade.NotificationsQuickSettingsContainer>
<ViewStub
android:id="@+id/keyguard_user_switcher_stub"
android:layout="@layout/keyguard_user_switcher"
android:layout_height="match_parent"
android:layout_width="match_parent" />
<include layout="@layout/dock_info_bottom_area_overlay" />
<com.android.keyguard.LockIconView
android:id="@+id/lock_icon_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<!-- Background protection -->
<ImageView
android:id="@+id/lock_icon_bg"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/fingerprint_bg"
android:visibility="invisible"/>
<ImageView
android:id="@+id/lock_icon"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:scaleType="centerCrop"/>
</com.android.keyguard.LockIconView>
<include
layout="@layout/keyguard_bottom_area"
android:visibility="gone" />
</com.android.systemui.shade.NotificationPanelView>
qs_panel.xml
<com.android.systemui.qs.QSContainerImpl xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/quick_settings_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:clipChildren="false">
<com.android.systemui.qs.NonInterceptingScrollView
android:id="@+id/expanded_qs_scroll_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="@dimen/qs_panel_elevation"
android:importantForAccessibility="no"
android:scrollbars="none"
android:clipChildren="false"
android:clipToPadding="false"
android:layout_weight="1">
<com.android.systemui.qs.QSPanel
android:id="@+id/quick_settings_panel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:focusable="true"
android:accessibilityTraversalBefore="@android:id/edit"
android:clipToPadding="false"
android:clipChildren="false">
<include layout="@layout/qs_footer_impl" />
</com.android.systemui.qs.QSPanel>
</com.android.systemui.qs.NonInterceptingScrollView>
<include layout="@layout/quick_status_bar_expanded_header" />
<include
layout="@layout/footer_actions"
android:id="@+id/qs_footer_actions"
android:layout_height="@dimen/footer_actions_height"
android:layout_width="match_parent"
android:layout_gravity="bottom"
/>
<include
android:id="@+id/qs_customize"
layout="@layout/qs_customize_panel"
android:visibility="gone" />
</com.android.systemui.qs.QSContainerImpl>
qs_footer_impl.xml
<!-- Extends FrameLayout -->
<com.android.systemui.qs.QSFooterView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/qs_footer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/qs_footer_margin"
android:layout_marginEnd="@dimen/qs_footer_margin"
android:layout_marginBottom="@dimen/qs_footers_margin_bottom"
android:background="@android:color/transparent"
android:baselineAligned="false"
android:clickable="false"
android:clipChildren="false"
android:clipToPadding="false">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/qs_footer_height"
android:layout_gravity="center_vertical">
<TextView
android:id="@+id/build"
android:layout_width="0dp"
android:layout_height="match_parent"
android:paddingEnd="4dp"
android:layout_weight="1"
android:clickable="true"
android:ellipsize="marquee"
android:focusable="true"
android:gravity="center_vertical"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.QS.Status.Build"
android:visibility="gone" />
<com.android.systemui.qs.PageIndicator
android:id="@+id/footer_page_indicator"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center_vertical"
android:visibility="gone" />
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<com.android.systemui.statusbar.AlphaOptimizedImageView
android:id="@android:id/edit"
android:layout_width="@dimen/qs_footer_action_button_size"
android:layout_height="@dimen/qs_footer_action_button_size"
android:layout_gravity="center_vertical|end"
android:background="@drawable/qs_footer_edit_circle"
android:clickable="true"
android:contentDescription="@string/accessibility_quick_settings_edit"
android:focusable="true"
android:padding="@dimen/qs_footer_icon_padding"
android:src="@*android:drawable/ic_mode_edit"
android:tint="?android:attr/textColorPrimary" />
</FrameLayout>
</LinearLayout>
</com.android.systemui.qs.QSFooterView>
3、QSB状态栏
combined_qs_header.xml
<!--
keep split_shade_status_bar height constant to avoid requestLayout calls on each
frame when animating QS <-> QQS transition
-->
<com.android.systemui.util.NoRemeasureMotionLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/split_shade_status_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/qs_header_height"
android:minHeight="@dimen/large_screen_shade_header_min_height"
android:clickable="false"
android:focusable="true"
android:paddingLeft="@dimen/qs_panel_padding"
android:paddingRight="@dimen/qs_panel_padding"
android:visibility="gone"
android:theme="@style/Theme.SystemUI.QuickSettings.Header"
app:layoutDescription="@xml/combined_qs_header_scene">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/qqs_header_bottom_guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_begin="@dimen/large_screen_shade_header_min_height"
/>
<androidx.constraintlayout.widget.Guideline
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/begin_guide"
android:orientation="vertical"
app:layout_constraintGuide_begin="0dp"/>
<androidx.constraintlayout.widget.Guideline
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/end_guide"
android:orientation="vertical"
app:layout_constraintGuide_end="0dp"
/>
<androidx.constraintlayout.widget.Guideline
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/center_left"
android:orientation="vertical" />
<androidx.constraintlayout.widget.Guideline
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/center_right"
android:orientation="vertical" />
<androidx.constraintlayout.widget.Barrier
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/barrier"
app:barrierDirection="start"
app:constraint_referenced_ids="statusIcons,privacy_container" />
<com.android.systemui.statusbar.policy.Clock
android:id="@+id/clock"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:gravity="start|center_vertical"
android:paddingStart="@dimen/status_bar_left_clock_starting_padding"
android:paddingEnd="@dimen/status_bar_left_clock_end_padding"
android:singleLine="true"
android:textDirection="locale"
android:textAppearance="@style/TextAppearance.QS.Status"
android:transformPivotX="0dp"
android:transformPivotY="24dp"
android:scaleX="1"
android:scaleY="1"
/>
<com.android.systemui.statusbar.policy.VariableDateView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_gravity="start|center_vertical"
android:gravity="center_vertical"
android:singleLine="true"
android:textDirection="locale"
android:textAppearance="@style/TextAppearance.QS.Status"
app:longDatePattern="@string/abbrev_wday_month_day_no_year_alarm"
app:shortDatePattern="@string/abbrev_month_day_no_year"
/>
<include
android:id="@+id/carrier_group"
layout="@layout/shade_carrier_group"
app:layout_constraintHeight_min="@dimen/large_screen_shade_header_min_height"
android:minHeight="@dimen/large_screen_shade_header_min_height"
app:layout_constraintWidth_min="48dp"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constrainedWidth="true"
android:layout_gravity="end|center_vertical"
android:layout_marginStart="8dp"
app:layout_constraintStart_toEndOf="@id/date"
app:layout_constraintEnd_toStartOf="@id/statusIcons"
app:layout_constraintTop_toTopOf="@id/clock"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="1"
/>
<com.android.systemui.statusbar.phone.StatusIconContainer
android:id="@+id/statusIcons"
app:layout_constraintHeight_min="@dimen/large_screen_shade_header_min_height"
android:paddingEnd="@dimen/signal_cluster_battery_padding"
android:layout_width="wrap_content"
android:layout_height="@dimen/large_screen_shade_header_min_height"
app:layout_constraintStart_toEndOf="@id/carrier_group"
app:layout_constraintEnd_toStartOf="@id/batteryRemainingIcon"
app:layout_constraintTop_toTopOf="@id/clock"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_bias="1"
/>
<com.android.systemui.battery.BatteryMeterView
android:id="@+id/batteryRemainingIcon"
android:layout_width="wrap_content"
android:layout_height="@dimen/large_screen_shade_header_min_height"
app:layout_constraintHeight_min="@dimen/large_screen_shade_header_min_height"
app:layout_constrainedWidth="true"
app:textAppearance="@style/TextAppearance.QS.Status"
app:layout_constraintStart_toEndOf="@id/statusIcons"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/clock"
app:layout_constraintBottom_toBottomOf="parent"
/>
<FrameLayout
android:id="@+id/privacy_container"
android:layout_width="wrap_content"
android:layout_height="@dimen/large_screen_shade_header_min_height"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="@id/date"
app:layout_constraintEnd_toEndOf="@id/end_guide"
app:layout_constraintTop_toTopOf="@id/date">
<com.android.systemui.privacy.OngoingPrivacyChip
android:layout_width="wrap_content"
android:layout_height="match_parent" />
</FrameLayout>
</com.android.systemui.util.NoRemeasureMotionLayout>
4、桌面状态栏
status_bar.xml
<!-- android:background="@drawable/status_bar_closed_default_background" -->
<com.android.systemui.statusbar.phone.PhoneStatusBarView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:systemui="http://schemas.android.com/apk/res/com.android.systemui"
android:layout_width="match_parent"
android:layout_height="@dimen/status_bar_height"
android:id="@+id/status_bar"
android:orientation="vertical"
android:focusable="false"
android:descendantFocusability="afterDescendants"
android:accessibilityPaneTitle="@string/status_bar"
>
<ImageView
android:id="@+id/notification_lights_out"
android:layout_width="@dimen/status_bar_icon_size"
android:layout_height="match_parent"
android:paddingStart="@dimen/status_bar_padding_start"
android:paddingBottom="2dip"
android:src="@drawable/ic_sysbar_lights_out_dot_small"
android:scaleType="center"
android:visibility="gone"
/>
<LinearLayout android:id="@+id/status_bar_contents"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
android:paddingStart="@dimen/status_bar_padding_start"
android:paddingEnd="@dimen/status_bar_padding_end"
android:paddingTop="@dimen/status_bar_padding_top"
android:orientation="horizontal">
<!-- Container for the entire start half of the status bar. It will always use the same
width, independent of the number of visible children and sub-children. -->
<FrameLayout
android:id="@+id/status_bar_start_side_container"
android:layout_height="match_parent"
android:layout_width="0dp"
android:clipChildren="false"
android:layout_weight="1">
<!-- Container that is wrapped around the views on the start half of the status bar.
Its width will change with the number of visible children and sub-children.
It is useful when we want to know the visible bounds of the content. -->
<FrameLayout
android:id="@+id/status_bar_start_side_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipChildren="false">
<include layout="@layout/heads_up_status_bar_layout" />
<!-- The alpha of the start side is controlled by PhoneStatusBarTransitions, and the
individual views are controlled by StatusBarManager disable flags DISABLE_CLOCK
and DISABLE_NOTIFICATION_ICONS, respectively -->
<LinearLayout
android:id="@+id/status_bar_start_side_except_heads_up"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:clipChildren="false">
<ViewStub
android:id="@+id/operator_name"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout="@layout/operator_name" />
<com.android.systemui.statusbar.policy.Clock
android:id="@+id/clock"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textAppearance="@style/TextAppearance.StatusBar.Clock"
android:singleLine="true"
android:paddingStart="@dimen/status_bar_left_clock_starting_padding"
android:paddingEnd="@dimen/status_bar_left_clock_end_padding"
android:gravity="center_vertical|start"
/>
<include layout="@layout/ongoing_call_chip" />
<com.android.systemui.statusbar.AlphaOptimizedFrameLayout
android:id="@+id/notification_icon_area"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
android:clipChildren="false"/>
</LinearLayout>
</FrameLayout>
</FrameLayout>
<!-- Space should cover the notch (if it exists) and let other views lay out around it -->
<android.widget.Space
android:id="@+id/cutout_space_view"
android:layout_width="0dp"
android:layout_height="match_parent"
android:gravity="center_horizontal|center_vertical"
/>
<!-- Container for the entire end half of the status bar. It will always use the same
width, independent of the number of visible children and sub-children. -->
<FrameLayout
android:id="@+id/status_bar_end_side_container"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:clipChildren="false">
<!-- Container that is wrapped around the views on the end half of the
status bar. Its width will change with the number of visible children and
sub-children.
It is useful when we want know the visible bounds of the content.-->
<com.android.keyguard.AlphaOptimizedLinearLayout
android:id="@+id/status_bar_end_side_content"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
android:orientation="horizontal"
android:gravity="center_vertical|end"
android:clipChildren="false">
<include
android:id="@+id/user_switcher_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/status_bar_user_chip_end_margin"
layout="@layout/status_bar_user_chip_container" />
<include layout="@layout/system_icons" />
</com.android.keyguard.AlphaOptimizedLinearLayout>
</FrameLayout>
</LinearLayout>
</com.android.systemui.statusbar.phone.PhoneStatusBarView>
5、QSB编辑页面
status_bar_expanded.xml
qs_panel.xml
<com.android.systemui.qs.QSContainerImpl xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/quick_settings_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:clipChildren="false">
<com.android.systemui.qs.NonInterceptingScrollView
android:id="@+id/expanded_qs_scroll_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="@dimen/qs_panel_elevation"
android:importantForAccessibility="no"
android:scrollbars="none"
android:clipChildren="false"
android:clipToPadding="false"
android:layout_weight="1">
<com.android.systemui.qs.QSPanel
android:id="@+id/quick_settings_panel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:focusable="true"
android:accessibilityTraversalBefore="@android:id/edit"
android:clipToPadding="false"
android:clipChildren="false">
<include layout="@layout/qs_footer_impl" />
</com.android.systemui.qs.QSPanel>
</com.android.systemui.qs.NonInterceptingScrollView>
<include layout="@layout/quick_status_bar_expanded_header" />
<include
layout="@layout/footer_actions"
android:id="@+id/qs_footer_actions"
android:layout_height="@dimen/footer_actions_height"
android:layout_width="match_parent"
android:layout_gravity="bottom"
/>
<include
android:id="@+id/qs_customize"
layout="@layout/qs_customize_panel"
android:visibility="gone" />
</com.android.systemui.qs.QSContainerImpl>
qs_customize_panel.xml
<!-- Height is 0 because it will be managed by the QS manually -->
<com.android.systemui.qs.customize.QSCustomizer
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="0dp"
android:elevation="4dp"
android:orientation="vertical"
android:gravity="center_horizontal">
</com.android.systemui.qs.customize.QSCustomizer>
QSCustomizer.java
public QSCustomizer(Context context, AttributeSet attrs) {
super(context, attrs);
LayoutInflater.from(getContext()).inflate(R.layout.qs_customize_panel_content, this);
mClipper = new QSDetailClipper(findViewById(R.id.customize_container));
Toolbar toolbar = findViewById(com.android.internal.R.id.action_bar);
TypedValue value = new TypedValue();
mContext.getTheme().resolveAttribute(android.R.attr.homeAsUpIndicator, value, true);
toolbar.setNavigationIcon(
getResources().getDrawable(value.resourceId, mContext.getTheme()));
toolbar.getMenu().add(Menu.NONE, MENU_RESET, 0, com.android.internal.R.string.reset)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
toolbar.setTitle(R.string.qs_edit);
mRecyclerView = findViewById(android.R.id.list);
mTransparentView = findViewById(R.id.customizer_transparent_view);
DefaultItemAnimator animator = new DefaultItemAnimator();
animator.setMoveDuration(TileAdapter.MOVE_DURATION);
mRecyclerView.setItemAnimator(animator);
updateTransparentViewHeight();
}
qs_customize_panel_content.xml
<merge xmlns:android="http://schemas.android.com/apk/res/android">->
<View
android:id="@+id/customizer_transparent_view"
android:layout_width="match_parent"
android:layout_height="@dimen/qs_header_system_icons_area_height"
android:background="@android:color/transparent" />
<com.android.keyguard.AlphaOptimizedLinearLayout
android:id="@+id/customize_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical"
android:paddingStart="@dimen/qs_customize_internal_side_paddings"
android:paddingEnd="@dimen/qs_customize_internal_side_paddings"
android:background="@drawable/qs_customizer_background">
<Toolbar
android:id="@*android:id/action_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/qs_customizer_toolbar"
android:navigationContentDescription="@*android:string/action_bar_up_description"
style="@style/QSCustomizeToolbar"
/>
<androidx.recyclerview.widget.RecyclerView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:paddingStart="@dimen/qs_customize_internal_side_paddings"
android:paddingEnd="@dimen/qs_customize_internal_side_paddings"
android:paddingBottom="28dp"
android:clipChildren="false"
android:clipToPadding="false"
android:scrollIndicators="top"
android:scrollbars="vertical"
android:scrollbarStyle="outsideOverlay"
android:importantForAccessibility="auto" />
</com.android.keyguard.AlphaOptimizedLinearLayout>
<View
android:id="@+id/nav_bar_background"
android:layout_width="match_parent"
android:layout_height="@dimen/navigation_bar_size"
android:layout_gravity="bottom"
android:background="#ff000000" />
</merge>
B ---------->SystemUI启动流程分析
BootLoader:启动加载器是设备启动过程的第一阶段,它初始化硬件并加载操作系统内核。
Linux Kernel:内核是操作系统的核心,负责管理系统资源,如内存、处理器和设备驱动程序。
init:init 是系统启动后运行的第一个用户空间进程(进程ID为1)。它负责启动系统上的其他进程和服务。
Zygote:Zygote是Android中的一个特殊进程,它启动后会加载Java核心库,并为其他应用进程提供服务。它通过fork操作来创建新的应用进程,Zygote 进程在启动后,会创建另一个重要的进程SystemServer进程。
SystemServer:系统服务器是Android中一个关键的守护进程,它启动了Android系统的核心服务,如窗口管理器、活动管理器、包管理器等。
ActivityManagerService:活动管理器服务负责管理应用程序的活动(Activity)生命周期,并处理应用程序的启动和切换
简而言之:
设备开机,BootLoader启动。
加载Linux内核。
内核初始化后,启动init进程。
init进程启动Zygote进程。
Zygote进程启动后,会加载核心库并准备创建应用进程。
系统服务器(SystemServer)启动,它将启动一系列系统服务。
SystemUI启动
SystemServer由ZygoteInit进程创建并启动
于是走到SystemUIService的onCreate()方法
这样相关类就注入结束了,接着回到 SystemUIApplication 的 startServicesIfNeeded() 方法
到此 CoreStartable (SystemUI) 启动流程分析完毕。
C ---------->SystemUI屏幕亮度调节代码分析
上面布局分析没分析这个亮度调节栏,就放这分析吧。
quick_settings_brightness_dialog.xml
<com.android.systemui.settings.brightness.BrightnessSliderView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/brightness_slider"
android:layout_width="match_parent"
android:layout_height="@dimen/brightness_mirror_height"
android:layout_gravity="center"
android:contentDescription="@string/accessibility_brightness"
android:importantForAccessibility="no" >
<com.android.systemui.settings.brightness.ToggleSeekBar
android:id="@+id/slider"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:minHeight="48dp"
android:thumb="@null"
android:background="@null"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:progressDrawable="@drawable/brightness_progress_drawable"
android:splitTrack="false"
/>
</com.android.systemui.settings.brightness.BrightnessSliderView>
BrightnessSliderView.java
// Inflated from quick_settings_brightness_dialog
@Override
protected void onFinishInflate() {
super.onFinishInflate();
setLayerType(LAYER_TYPE_HARDWARE, null);
mSlider = requireViewById(R.id.slider);
mSlider.setAccessibilityLabel(getContentDescription().toString());
// Finds the progress drawable. Assumes brightness_progress_drawable.xml
try {
LayerDrawable progress = (LayerDrawable) mSlider.getProgressDrawable();
DrawableWrapper progressSlider = (DrawableWrapper) progress
.findDrawableByLayerId(android.R.id.progress);
LayerDrawable actualProgressSlider = (LayerDrawable) progressSlider.getDrawable();
mProgressDrawable = actualProgressSlider.findDrawableByLayerId(R.id.slider_foreground);
} catch (Exception e) {
// Nothing to do, mProgressDrawable will be null.
}
}
BrightnessController.java
private volatile boolean mAutomatic; // Brightness adjusted automatically using ambient light.
private volatile boolean mIsVrModeEnabled;
private boolean mListening;
private boolean mExternalChange;
private boolean mControlValueInitialized;
private float mBrightnessMin = PowerManager.BRIGHTNESS_MIN;
private float mBrightnessMax = PowerManager.BRIGHTNESS_MAX;
mAutomatic:屏幕亮度是否根据环境光传感器自动调整。
mListening:是否正在监听外部事件
mIsVrModeEnabled:是否启用了虚拟现实(VR)模式(不同亮度的设置)。
mExternalChange:追踪亮度变化是否由外部因素引起,而非用户直接通过滑块进行调整。
mControlValueInitialized:记控制值是否已经被初始化。这确保在应用亮度设置之前,相关的值已经被正确设置。
mBrightnessMin:存储亮度的最小值,默认值为PowerManager.BRIGHTNESS_MIN,这是系统定义的亮度最小值常量。
mBrightnessMax:存储亮度的最大值,默认值为PowerManager.BRIGHTNESS_MAX,这是系统定义的亮度最大值常量。
这些变量是控制亮度逻辑的重要组成部分,它们使得BrightnessSliderView类能够根据不同的条件和用户交互来调整屏幕上的亮度。
BrightnessController.java
private void updateSlider(float brightnessValue, boolean inVrMode) {
final float min = mBrightnessMin;
final float max = mBrightnessMax;
// Ensure the slider is in a fixed position first, then check if we should animate.
if (mSliderAnimator != null && mSliderAnimator.isStarted()) {
mSliderAnimator.cancel();
}
// convertGammaToLinearFloat returns 0-1
if (BrightnessSynchronizer.floatEquals(brightnessValue,
convertGammaToLinearExt(mControl.getValue(), min, max))) {//[CH108Q][62218]change by chenchuanliang for brightness change smooth merge from CH108Q2022060
// If the value in the slider is equal to the value on the current brightness
// then the slider does not need to animate, since the brightness will not change.
return;
}
// Returns GAMMA_SPACE_MIN - GAMMA_SPACE_MAX
//[CH108Q][62218]change by chenchuanliang for brightness change smooth merge from CH108Q20220602
final int sliderVal = convertLinearToGammaExt(brightnessValue, min, max);
animateSliderTo(sliderVal);
}
亮度最小值由PowerManager.BRIGHTNESS_MIN控制
D ---------->SystemUI之QS面板分析
1、QS面板构成元素分析
QS面板实际上有多种状态,包括:
1、Quick Quick Settings (QQS) : 即初级展开面板,是一次下拉面板看到的简版QS面板,包含少量的开关,如下左侧的图。
2、Quick Settings (QS) : 完整QS面板,是二次下拉面板看到的完成QS面板,其包含更多的开关,如下右侧的图。
3、另外还有开关编辑面板,开关详情页面。
注:SystemUI中称通知栏下拉面板开关区域中的单个开关为Tile,下面进行分析QS面板中主要的几大类簇。
1、QSTile类簇
packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTile.java
packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileImpl.java
packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java
packages/SystemUI/src/com/android/systemui/qs/tiles/XxxTile.java
该类簇构成了的Tile的"后端",负责处理单个Tile的逻辑处理,其中:
QSTile:接口,主要定义了所有Tile的通用行为,如注册监听、点击事件的处理,Tile视图中Icon元素(QSIconView)的构建,刷新Tile状态等。
QSTileImpl:实现了QSTile 定义的通用行为,同时提供了一系列的抽象接口(详见类图)允许不同类型的子类去做差异化实现。
后续所有的开关都需要继承自QSTileImpl。如BluetoothTile通过继承QSTileImpl来享用其提供的通用方法,不需要每个开关都去做实现,
同时利用差异化接口便可实现开关自身的特有逻辑,如BluetoothTile的handleClick则可以打开蓝牙开关。
2、QSTileView类簇
packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSTileView.java
packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSIconView.java
packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSTileViewImpl.kt
packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
该类簇构成了的Tile的视图层,负责处理单个Tile的界面展示,其中:
QSTileView:抽象类,定义了Tile视图相关的操作,如返回只包含图标的QSIconView、视图刷新onStateChanged(State state)等。
在其构建的视图的基础上,扩展了label相关的东西,label即为开关中的文字描述视图。
QSTileViewImpl:实现了QSTileView中定义的抽象接口,同时在其构造方法中完成了Tile视图的构建,包括背景的处理、点击效果的处理(如ripple)、点击事件的处理等。
3、QSHost类簇
packages/SystemUI/src/com/android/systemui/qs/QSHost.java
packages/SystemUI/src/com/android/systemui/qs/QSTileHost.java
packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QSFactory.java
packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
该类簇主要完成单个Tile的构建,其中:
QSHost:接口,主要向外界提供获取QSTile集合的接口(getTiles())以及作为Tile对外沟通的桥梁,例如点击某个开关后需要触发收起面板的操作,开关便会通过QSHost来触发收起面板的操作。
QSTileHost :实现了QSHost中定义的接口,同时扩展了创建Tile后端对象QSTile和创建Tile视图对象QSTileView的接口。创建时使用了工厂模式,由QSFactoryImpl类实现
其中QSTileHost作为外界创建Tile的入口,会在对象构造的过程中先去创建Tile后端对象QSTile集合,这个集合在后续创建完整Tile对象时会用到。 具体创建哪些Tile则是通过获取配置在 config.xml 中的字段来决定了。具体过程可查看 QSTileHost.onTuningChanged(String key, String newValue) 方法。
config.xml
<!-- The default tiles to display in QuickSettings -->
<string name="quick_settings_tiles_default" translatable="false">
internet,bt,flashlight,dnd,alarm,airplane,controls,wallet,rotation,battery,cast,screenrecord,screenshot,mictoggle,cameratoggle,custom(com.android.permissioncontroller/.permission.service.SafetyCenterQsTileService)
</string><!-- luzongrui add screenshot tile 20240328-->
4、QSPanel类簇
packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java
packages/SystemUI/src/com/android/systemui/qs/QSTileLayout
packages/SystemUI/src/com/android/systemui/qs/TileLayout.java
packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
该类簇主要完成QS面板元素的动态添加,其中:
QSPanel:对应我们前面说的QS面板,即二次展开面板,是该页面的顶层容器,其嵌套在一个ScrollView中. 负责动态添加相应的元素,添加的元素包括 [亮度条 / 根据屏幕方向动态添加开关容器 QSTileLayout / Footer ] 等。
同时为这些元素提供了一系列的操作接口,如:
1、为开关容器创建具体开关对象(通过QSHost) 并为其添加刷新监听器,在后续开关后端收到开关状态刷新需求时,将刷新需求分发到对应的开关视图层.
2、开关详情页(Details)的创建与刷新
3、QS面板展开状态变化时做相应的处理(setExpanded(boolean expanded))
QuickQSPanel:对应我们前面说的初级展开面板QQS面板,继承自QSPanel并对展示的Tile数做了限制(通过setMaxTiles(int)),
同时复写了父类提供的添加子元素的方法,按需添加QQS面板的元素,因为QQS面板是QS面板的精简版,所以很多子元素未做添加。
QSTileLayout:开关容器接口,主要定义了开关容器绘制相关的接口。
TileLayout:QQS面板开关容器类,负责精简QS面板的绘制,继承自 Viewgroup
PagedTileLayout:QS面板开关容器类,负责QS面板的绘制,继承自 ViewPager
5、QS类簇
packages/SystemUI/plugin/src/com/android/systemui/plugins/qs/QS.java
packages/SystemUI/src/com/android/systemui/qs/QSFragment.java
packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java
该类簇主要作为整个QS面板的顶层容器,主要处理QS面板的展开/收起逻辑,其中:
QS:接口,主要定义了 QS 面板展开/收起相关的接口。
QSFragment:继承自 Fragment,实现了 QS 接口,主要负责接收 QS 面板展开/收起状态的改变,并将最新状态同步给该 Fragment 中的 View 元素,如 QSContainerImpl。
QSContainerImpl:添加到QSFragment中的自定义 View,继承自FrameLayout对应布局 qs_panel.xml ,主要通过接收来自 QSFragment 的面板展开/收起状态的变化并做刷新。
2、QS面板内部实现梳理
a:QS面板开关集合构建流程
QsPanel 中除了创建各个开关View,还创建了亮度条,Footer等元素。另外QsFragment / QSHost 等元素是在 SystemUI 启动流程中通过注入或反射构建的,其前期构建流程跳过分析,先看看整体的流程图。
简单说就是QSTileHost对象在构建初期就借助QSFactoryImpl工具对象提前创建好了各个开关的后端对象QSTile,而后QSPanel在初始化的过程中,再次利用QSTileHost去构建各个开关的视图对象QSTileView,至此一个完整的开关就构建完成,最后add到开关容器PagedTileLayout中去。
b:Tile后端是如何与Tile视图层联系的
前面QSTile的类簇中我们可以看到其有多个内部类,与此相关的内部类包括 Callback 和 State。
Tile 视图与后端的联系就是借助这两个内部类以及QSPanel这个中介产生联系的。
前面 在梳理QS面板开关集合构建流程时可以看到步骤10通过addTile函数来构建Tile开关对象,其代码细节如下:
QSPanel.java
final void addTile(QSPanelControllerBase.TileRecord tileRecord) {
final QSTile.Callback callback = new QSTile.Callback() {
@Override
public void onStateChanged(QSTile.State state) {
drawTile(tileRecord, state);
}
};
tileRecord.tile.addCallback(callback);
tileRecord.callback = callback;
tileRecord.tileView.init(tileRecord.tile);
tileRecord.tile.refreshState();
if (mTileLayout != null) {
mTileLayout.addTile(tileRecord);
}
}
可以看到QSPanel向 tileRecord.tile 即后端注册了一个回调器,并在回调发生时将开关状态State传递给 tileRecord.tileView, 即开关视图层去做视图刷新。
c: Tile的一次点击事件背后的流程是怎么样的
上面介绍addTile函数时可以看到一句tileRecord.tileView.init(tileRecord.tile);这里完成了将视图层的点击事件转交给后端的操作。
QSTileViewImpl.kt
override fun init(tile: QSTile) {
init(
{ v: View? -> tile.click(this) },
{ view: View? ->
tile.longClick(this)
true
}
)
}
private fun init(
click: OnClickListener?,
longClick: OnLongClickListener?
) {
setOnClickListener(click)
onLongClickListener = longClick
}
即将QSTileView收到的点击事件或长按事件分别交给QSTile对应的点击、长按函数处理。
QSTile.java是一个接口,具体的实现逻辑是在QSTileImpl.java中。
QSTileImpl.java
public void click(@Nullable View view) {
mMetricsLogger.write(populate(new LogMaker(ACTION_QS_CLICK).setType(TYPE_ACTION)
.addTaggedData(FIELD_STATUS_BAR_STATE,
mStatusBarStateController.getState())));
mUiEventLogger.logWithInstanceId(QSEvent.QS_ACTION_CLICK, 0, getMetricsSpec(),
getInstanceId());
final int eventId = mClickEventId++;
mQSLogger.logTileClick(mTileSpec, mStatusBarStateController.getState(), mState.state,
eventId);
if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
mHandler.obtainMessage(H.CLICK, eventId, 0, view).sendToTarget();
}
}
public void secondaryClick(@Nullable View view) {
mMetricsLogger.write(populate(new LogMaker(ACTION_QS_SECONDARY_CLICK).setType(TYPE_ACTION)
.addTaggedData(FIELD_STATUS_BAR_STATE,
mStatusBarStateController.getState())));
mUiEventLogger.logWithInstanceId(QSEvent.QS_ACTION_SECONDARY_CLICK, 0, getMetricsSpec(),
getInstanceId());
final int eventId = mClickEventId++;
mQSLogger.logTileSecondaryClick(mTileSpec, mStatusBarStateController.getState(),
mState.state, eventId);
mHandler.obtainMessage(H.SECONDARY_CLICK, eventId, 0, view).sendToTarget();
}
@Override
public void longClick(@Nullable View view) {
mMetricsLogger.write(populate(new LogMaker(ACTION_QS_LONG_PRESS).setType(TYPE_ACTION)
.addTaggedData(FIELD_STATUS_BAR_STATE,
mStatusBarStateController.getState())));
mUiEventLogger.logWithInstanceId(QSEvent.QS_ACTION_LONG_PRESS, 0, getMetricsSpec(),
getInstanceId());
final int eventId = mClickEventId++;
mQSLogger.logTileLongClick(mTileSpec, mStatusBarStateController.getState(), mState.state,
eventId);
mHandler.obtainMessage(H.LONG_CLICK, eventId, 0, view).sendToTarget();
}
QSTileImpl.java
protected final class H extends Handler {
private static final int ADD_CALLBACK = 1;
private static final int CLICK = 2;
private static final int SECONDARY_CLICK = 3;
private static final int LONG_CLICK = 4;
private static final int REFRESH_STATE = 5;
private static final int USER_SWITCH = 6;
private static final int DESTROY = 7;
private static final int REMOVE_CALLBACKS = 8;
private static final int REMOVE_CALLBACK = 9;
private static final int SET_LISTENING = 10;
@VisibleForTesting
protected static final int STALE = 11;
private static final int INITIALIZE = 12;
@VisibleForTesting
protected H(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
String name = null;
try {
if (msg.what == ADD_CALLBACK) {
name = "handleAddCallback";
handleAddCallback((QSTile.Callback) msg.obj);
} else if (msg.what == REMOVE_CALLBACKS) {
name = "handleRemoveCallbacks";
handleRemoveCallbacks();
} else if (msg.what == REMOVE_CALLBACK) {
name = "handleRemoveCallback";
handleRemoveCallback((QSTile.Callback) msg.obj);
} else if (msg.what == CLICK) {
name = "handleClick";
if (mState.disabledByPolicy) {
Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent(
mContext, mEnforcedAdmin);
mActivityStarter.postStartActivityDismissingKeyguard(intent, 0);
} else {
mQSLogger.logHandleClick(mTileSpec, msg.arg1);
handleClick((View) msg.obj);
}
} else if (msg.what == SECONDARY_CLICK) {
name = "handleSecondaryClick";
mQSLogger.logHandleSecondaryClick(mTileSpec, msg.arg1);
handleSecondaryClick((View) msg.obj);
} else if (msg.what == LONG_CLICK) {
name = "handleLongClick";
mQSLogger.logHandleLongClick(mTileSpec, msg.arg1);
handleLongClick((View) msg.obj);
} else if (msg.what == REFRESH_STATE) {
name = "handleRefreshState";
handleRefreshState(msg.obj);
} else if (msg.what == USER_SWITCH) {
name = "handleUserSwitch";
handleUserSwitch(msg.arg1);
} else if (msg.what == DESTROY) {
name = "handleDestroy";
handleDestroy();
} else if (msg.what == SET_LISTENING) {
name = "handleSetListeningInternal";
handleSetListeningInternal(msg.obj, msg.arg1 != 0);
} else if (msg.what == STALE) {
name = "handleStale";
handleStale();
} else if (msg.what == INITIALIZE) {
name = "initialize";
handleInitialize();
} else {
throw new IllegalArgumentException("Unknown msg: " + msg.what);
}
} catch (Throwable t) {
final String error = "Error in " + name;
Log.w(TAG, error, t);
}
}
}
protected final void handleRefreshState(@Nullable Object arg) {
handleUpdateState(mTmpState, arg);
boolean changed = mTmpState.copyTo(mState);
if (mReadyState == READY_STATE_READYING) {
mReadyState = READY_STATE_READY;
changed = true;
}
if (changed) {
mQSLogger.logTileUpdated(mTileSpec, mState);
handleStateChanged();
}
mHandler.removeMessages(H.STALE);
mHandler.sendEmptyMessageDelayed(H.STALE, getStaleTimeout());
setListening(mStaleListener, false);
}
private void handleStateChanged() {
if (mCallbacks.size() != 0) {
for (int i = 0; i < mCallbacks.size(); i++) {
mCallbacks.get(i).onStateChanged(mState);
}
}
}
可以看到QSTileImpl用一个State类型的临时变量去handleUpdateState函数中收集当前开关的最新状态,而这个函数是个抽象方法,实现依旧是在各个开关子类中,我们看下Bluetooth开关
BluetoothTile.java
@Override
protected void handleUpdateState(BooleanState state, Object arg) {
checkIfRestrictionEnforcedByAdminOnly(state, UserManager.DISALLOW_BLUETOOTH);
final boolean transientEnabling = arg == ARG_SHOW_TRANSIENT_ENABLING;
final boolean enabled = transientEnabling || mController.isBluetoothEnabled();
final boolean connected = mController.isBluetoothConnected();
final boolean connecting = mController.isBluetoothConnecting();
state.isTransient = transientEnabling || connecting ||
mController.getBluetoothState() == BluetoothAdapter.STATE_TURNING_ON;
if (!enabled || !connected || state.isTransient) {
stopListeningToStaleDeviceMetadata();
}
state.dualTarget = true;
state.value = enabled;
if (state.slash == null) {
state.slash = new SlashState();
}
state.slash.isSlashed = !enabled;
state.label = mContext.getString(R.string.quick_settings_bluetooth_label);
state.secondaryLabel = TextUtils.emptyIfNull(
getSecondaryLabel(enabled, connecting, connected, state.isTransient));
state.contentDescription = mContext.getString(
R.string.accessibility_quick_settings_bluetooth);
state.stateDescription = "";
if (enabled) {
if (connected) {
state.icon = ResourceIcon.get(R.drawable.qs_bluetooth_icon_on);
if (!TextUtils.isEmpty(mController.getConnectedDeviceName())) {
state.label = mController.getConnectedDeviceName();
}
state.stateDescription =
mContext.getString(R.string.accessibility_bluetooth_name, state.label)
+ ", " + state.secondaryLabel;
} else if (state.isTransient) {
state.icon = ResourceIcon.get(
R.drawable.qs_bluetooth_icon_search);
state.stateDescription = state.secondaryLabel;
} else {
state.icon =
ResourceIcon.get(R.drawable.qs_bluetooth_icon_off);
state.stateDescription = mContext.getString(R.string.accessibility_not_connected);
}
state.state = Tile.STATE_ACTIVE;
} else {
state.icon = ResourceIcon.get(R.drawable.qs_bluetooth_icon_off);
state.state = Tile.STATE_INACTIVE;
}
state.expandedAccessibilityClassName = Switch.class.getName();
}
handleUpdateState函数收集完开关状态后,如果开关状态发生了改变则会导致handleStateChanged() 被调用
QSTileImpl.java
private void handleStateChanged() {
if (mCallbacks.size() != 0) {
for (int i = 0; i < mCallbacks.size(); i++) {
mCallbacks.get(i).onStateChanged(mState);
}
}
}
QSPanel.java
final void addTile(QSPanelControllerBase.TileRecord tileRecord) {
final QSTile.Callback callback = new QSTile.Callback() {
@Override
public void onStateChanged(QSTile.State state) {
drawTile(tileRecord, state);
}
};
tileRecord.tile.addCallback(callback);
tileRecord.callback = callback;
tileRecord.tileView.init(tileRecord.tile);
tileRecord.tile.refreshState();
if (mTileLayout != null) {
mTileLayout.addTile(tileRecord);
}
}
这里就和前面(Tile后端是如何与Tile视图层联系的)关联起来了,即 state会被传递到视图层。
QSTileView.java
public abstract void onStateChanged(State state);
QSTileViewImpl.kt
override fun onStateChanged(state: QSTile.State) {
// We cannot use the handler here because sometimes, the views are not attached (if they
// are in a page that the ViewPager hasn't attached). Instead, we use a runnable where
// all its instances are `equal` to each other, so they can be used to remove them from the
// queue.
// This means that at any given time there's at most one enqueued runnable to change state.
// However, as we only ever care about the last state posted, this is fine.
val runnable = StateChangeRunnable(state.copy())
removeCallbacks(runnable)
post(runnable)
}
当Tile的状态发生变化时,创建一个任务来处理这个变化。通过 post 方法,状态更新的操作将在适当的时机异步执行。
inner class StateChangeRunnable(private val state: QSTile.State) : Runnable {
override fun run() {
handleStateChanged(state)
}
// We want all instances of this runnable to be equal to each other, so they can be used to
// remove previous instances from the Handler/RunQueue of this view
override fun equals(other: Any?): Boolean {
return other is StateChangeRunnable
}
// This makes sure that all instances have the same hashcode (because they are `equal`)
override fun hashCode(): Int {
return StateChangeRunnable::class.hashCode()
}
}
handleStateChanged函数是真正做开关视图显示刷新的地方
// HANDLE STATE CHANGES RELATED METHODS
protected open fun handleStateChanged(state: QSTile.State) {
val allowAnimations = animationsEnabled()
showRippleEffect = state.showRippleEffect
isClickable = state.state != Tile.STATE_UNAVAILABLE
isLongClickable = state.handlesLongClick
icon.setIcon(state, allowAnimations)
contentDescription = state.contentDescription
// State handling and description
val stateDescription = StringBuilder()
val arrayResId = SubtitleArrayMapping.getSubtitleId(state.spec)
val stateText = state.getStateText(arrayResId, resources)
state.secondaryLabel = state.getSecondaryLabel(stateText)
if (!TextUtils.isEmpty(stateText)) {
stateDescription.append(stateText)
}
if (state.disabledByPolicy && state.state != Tile.STATE_UNAVAILABLE) {
stateDescription.append(", ")
stateDescription.append(getUnavailableText(state.spec))
}
if (!TextUtils.isEmpty(state.stateDescription)) {
stateDescription.append(", ")
stateDescription.append(state.stateDescription)
if (lastState != INVALID && state.state == lastState &&
state.stateDescription != lastStateDescription) {
stateDescriptionDeltas = state.stateDescription
}
}
setStateDescription(stateDescription.toString())
lastStateDescription = state.stateDescription
accessibilityClass = if (state.state == Tile.STATE_UNAVAILABLE) {
null
} else {
state.expandedAccessibilityClassName
}
if (state is BooleanState) {
val newState = state.value
if (tileState != newState) {
tileState = newState
}
}
//
// Labels
if (!Objects.equals(label.text, state.label)) {
label.text = state.label
}
if (!Objects.equals(secondaryLabel.text, state.secondaryLabel)) {
secondaryLabel.text = state.secondaryLabel
secondaryLabel.visibility = if (TextUtils.isEmpty(state.secondaryLabel)) {
GONE
} else {
VISIBLE
}
}
// Colors
if (state.state != lastState || state.disabledByPolicy != lastDisabledByPolicy) {
singleAnimator.cancel()
mQsLogger?.logTileBackgroundColorUpdateIfInternetTile(
state.spec,
state.state,
state.disabledByPolicy,
getBackgroundColorForState(state.state, state.disabledByPolicy))
if (allowAnimations) {
singleAnimator.setValues(
colorValuesHolder(
BACKGROUND_NAME,
paintColor,
getBackgroundColorForState(state.state, state.disabledByPolicy)
),
colorValuesHolder(
LABEL_NAME,
label.currentTextColor,
getLabelColorForState(state.state, state.disabledByPolicy)
),
colorValuesHolder(
SECONDARY_LABEL_NAME,
secondaryLabel.currentTextColor,
getSecondaryLabelColorForState(state.state, state.disabledByPolicy)
),
colorValuesHolder(
CHEVRON_NAME,
chevronView.imageTintList?.defaultColor ?: 0,
getChevronColorForState(state.state, state.disabledByPolicy)
)
)
singleAnimator.start()
} else {
setAllColors(
getBackgroundColorForState(state.state, state.disabledByPolicy),
getLabelColorForState(state.state, state.disabledByPolicy),
getSecondaryLabelColorForState(state.state, state.disabledByPolicy),
getChevronColorForState(state.state, state.disabledByPolicy)
)
}
}
// Right side icon
loadSideViewDrawableIfNecessary(state)
label.isEnabled = !state.disabledByPolicy
lastState = state.state
lastDisabledByPolicy = state.disabledByPolicy
}
E ---------->SystemUI系统导航栏浅析
NavigationBar在CentralSurfacesImpl中调用start()时进行初始化
CentralSurfacesImpl.java
@Override
public void createAndAddWindows(@Nullable RegisterStatusBarResult result) {
makeStatusBarView(result);
mNotificationShadeWindowController.attach();
mStatusBarWindowController.attach();
}
protected void createNavigationBar(@Nullable RegisterStatusBarResult result) {
mNavigationBarController.createNavigationBars(true /* includeDefaultDisplay */, result);
}
NavigationBarController.java
/**
* Adds a navigation bar on default display or an external display if the display supports
* system decorations.
*
* @param display the display to add navigation bar on.
*/
@VisibleForTesting
void createNavigationBar(Display display, Bundle savedState, RegisterStatusBarResult result) {
if (display == null) {
return;
}
final int displayId = display.getDisplayId();
final boolean isOnDefaultDisplay = displayId == mDisplayTracker.getDefaultDisplayId();
if (!shouldCreateNavBarAndTaskBar(displayId)) {
return;
}
// We may show TaskBar on the default display for large screen device. Don't need to create
// navigation bar for this case.
if (isOnDefaultDisplay && initializeTaskbarIfNecessary()) {
return;
}
final Context context = isOnDefaultDisplay
? mContext
: mContext.createDisplayContext(display);
NavigationBarComponent component = mNavigationBarComponentFactory.create(
context, savedState);
NavigationBar navBar = component.getNavigationBar();
navBar.init();
mNavigationBars.put(displayId, navBar);
navBar.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
@Override
public void onViewAttachedToWindow(View v) {
if (result != null) {
navBar.setImeWindowStatus(display.getDisplayId(), result.mImeToken,
result.mImeWindowVis, result.mImeBackDisposition,
result.mShowImeSwitcher);
}
}
@Override
public void onViewDetachedFromWindow(View v) {
v.removeOnAttachStateChangeListener(this);
}
});
}
这个方法确保了在适当的条件下为每个显示设备创建导航栏。
private boolean shouldCreateNavBarAndTaskBar(int displayId) {
final IWindowManager wms = WindowManagerGlobal.getWindowManagerService();
try {
return wms.hasNavigationBar(displayId);
} catch (RemoteException e) {
// Cannot get wms, just return false with warning message.
Log.w(TAG, "Cannot get WindowManager.");
return false;
}
}
调用IWindowManager的hasNavigationBar方法来判断系统是否存在导航栏
NavigationBarComponent.java
package com.android.systemui.navigationbar;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.Nullable;
import com.android.systemui.dagger.qualifiers.DisplayId;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import javax.inject.Scope;
import dagger.BindsInstance;
import dagger.Subcomponent;
/**
* Subcomponent for a NavigationBar.
*
* Generally creatd on a per-display basis.
*/
@Subcomponent(modules = { NavigationBarModule.class })
@NavigationBarComponent.NavigationBarScope
public interface NavigationBarComponent {
/** Factory for {@link NavigationBarComponent}. */
@Subcomponent.Factory
interface Factory {
NavigationBarComponent create(
@BindsInstance @DisplayId Context context,
@BindsInstance @Nullable Bundle savedState);
}
/** */
NavigationBar getNavigationBar();
/**
* Scope annotation for singleton items within the NavigationBarComponent.
*/
@Documented
@Retention(RUNTIME)
@Scope
@interface NavigationBarScope {}
}
NavigationBarModule.java
package com.android.systemui.navigationbar;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.DisplayId;
import com.android.systemui.navigationbar.NavigationBarComponent.NavigationBarScope;
import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
import dagger.Module;
import dagger.Provides;
/** Module for {@link com.android.systemui.navigationbar.NavigationBarComponent}. */
@Module
public interface NavigationBarModule {
/** A Layout inflater specific to the display's context. */
@Provides
@NavigationBarScope
@DisplayId
static LayoutInflater provideLayoutInflater(@DisplayId Context context) {
return LayoutInflater.from(context);
}
/** */
@Provides
@NavigationBarScope
static NavigationBarFrame provideNavigationBarFrame(@DisplayId LayoutInflater layoutInflater) {
return (NavigationBarFrame) layoutInflater.inflate(R.layout.navigation_bar_window, null);
}
/** */
@Provides
@NavigationBarScope
static NavigationBarView provideNavigationBarview(
@DisplayId LayoutInflater layoutInflater, NavigationBarFrame frame) {
View barView = layoutInflater.inflate(R.layout.navigation_bar, frame);
return barView.findViewById(R.id.navigation_bar_view);
}
/** */
@Provides
@NavigationBarScope
static EdgeBackGestureHandler provideEdgeBackGestureHandler(
EdgeBackGestureHandler.Factory factory, @DisplayId Context context) {
return factory.create(context);
}
/** A WindowManager specific to the display's context. */
@Provides
@NavigationBarScope
@DisplayId
static WindowManager provideWindowManager(@DisplayId Context context) {
return context.getSystemService(WindowManager.class);
}
}
navigation_bar.xml
<com.android.systemui.navigationbar.NavigationBarView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/navigation_bar_view"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
android:background="@drawable/system_bar_background">
<com.android.systemui.navigationbar.NavigationBarInflaterView
android:id="@+id/navigation_inflater"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false" />
</com.android.systemui.navigationbar.NavigationBarView>
NavigationBarInflaterView.java
private void inflateChildren() {
removeAllViews();
mHorizontal = (FrameLayout) mLayoutInflater.inflate(R.layout.navigation_layout,
this /* root */, false /* attachToRoot */);
addView(mHorizontal);
mVertical = (FrameLayout) mLayoutInflater.inflate(R.layout.navigation_layout_vertical,
this /* root */, false /* attachToRoot */);
addView(mVertical);
updateAlternativeOrder();
}
下面以navigation_layout.xml文件为例
navigation_layout.xml
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:systemui="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/rounded_corner_content_padding"
android:layout_marginEnd="@dimen/rounded_corner_content_padding"
android:paddingStart="@dimen/nav_content_padding"
android:paddingEnd="@dimen/nav_content_padding"
android:clipChildren="false"
android:clipToPadding="false"
android:id="@+id/horizontal">
<com.android.systemui.navigationbar.buttons.NearestTouchFrame
android:id="@+id/nav_buttons"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
systemui:isVertical="false">
<LinearLayout
android:id="@+id/ends_group"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:clipToPadding="false"
android:clipChildren="false" />
<LinearLayout
android:id="@+id/center_group"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:gravity="center"
android:orientation="horizontal"
android:clipToPadding="false"
android:clipChildren="false" />
</com.android.systemui.navigationbar.buttons.NearestTouchFrame>
</FrameLayout>
NavigationBarInflaterView.java
@Override
protected void onFinishInflate() {
super.onFinishInflate();
inflateChildren();
clearViews();
inflateLayout(getDefaultLayout());
}
private void inflateChildren() {
removeAllViews();
mHorizontal = (FrameLayout) mLayoutInflater.inflate(R.layout.navigation_layout,
this /* root */, false /* attachToRoot */);
addView(mHorizontal);
mVertical = (FrameLayout) mLayoutInflater.inflate(R.layout.navigation_layout_vertical,
this /* root */, false /* attachToRoot */);
addView(mVertical);
updateAlternativeOrder();
}
addView在class ReverseLinearLayout extends LinearLayout中实现了方法的重写
addView(mHorizontal)将 mHorizontal 视图添加到当前 ViewGroup 中
protected void inflateLayout(String newLayout) {
mCurrentLayout = newLayout;
if (newLayout == null) {
newLayout = getDefaultLayout();
}
String[] sets = newLayout.split(GRAVITY_SEPARATOR, 3);
if (sets.length != 3) {
Log.d(TAG, "Invalid layout.");
newLayout = getDefaultLayout();
sets = newLayout.split(GRAVITY_SEPARATOR, 3);
}
String[] start = sets[0].split(BUTTON_SEPARATOR);
String[] center = sets[1].split(BUTTON_SEPARATOR);
String[] end = sets[2].split(BUTTON_SEPARATOR);
// Inflate these in start to end order or accessibility traversal will be messed up.
inflateButtons(start, mHorizontal.findViewById(R.id.ends_group),
false /* landscape */, true /* start */);
inflateButtons(start, mVertical.findViewById(R.id.ends_group),
true /* landscape */, true /* start */);
inflateButtons(center, mHorizontal.findViewById(R.id.center_group),
false /* landscape */, false /* start */);
inflateButtons(center, mVertical.findViewById(R.id.center_group),
true /* landscape */, false /* start */);
addGravitySpacer(mHorizontal.findViewById(R.id.ends_group));
addGravitySpacer(mVertical.findViewById(R.id.ends_group));
inflateButtons(end, mHorizontal.findViewById(R.id.ends_group),
false /* landscape */, false /* start */);
inflateButtons(end, mVertical.findViewById(R.id.ends_group),
true /* landscape */, false /* start */);
updateButtonDispatchersCurrentView();
}
private void inflateButtons(String[] buttons, ViewGroup parent, boolean landscape,
boolean start) {
for (int i = 0; i < buttons.length; i++) {
inflateButton(buttons[i], parent, landscape, start);
}
}
private ViewGroup.LayoutParams copy(ViewGroup.LayoutParams layoutParams) {
if (layoutParams instanceof LinearLayout.LayoutParams) {
return new LinearLayout.LayoutParams(layoutParams.width, layoutParams.height,
((LinearLayout.LayoutParams) layoutParams).weight);
}
return new LayoutParams(layoutParams.width, layoutParams.height);
}
@Nullable
protected View inflateButton(String buttonSpec, ViewGroup parent, boolean landscape,
boolean start) {
LayoutInflater inflater = landscape ? mLandscapeInflater : mLayoutInflater;
View v = createView(buttonSpec, parent, inflater);
if (v == null) return null;
v = applySize(v, buttonSpec, landscape, start);
parent.addView(v);
addToDispatchers(v);
View lastView = landscape ? mLastLandscape : mLastPortrait;
View accessibilityView = v;
if (v instanceof ReverseRelativeLayout) {
accessibilityView = ((ReverseRelativeLayout) v).getChildAt(0);
}
if (lastView != null) {
accessibilityView.setAccessibilityTraversalAfter(lastView.getId());
}
if (landscape) {
mLastLandscape = accessibilityView;
} else {
mLastPortrait = accessibilityView;
}
return v;
}
View createView(String buttonSpec, ViewGroup parent, LayoutInflater inflater) {
View v = null;
String button = extractButton(buttonSpec);
if (LEFT.equals(button)) {
button = extractButton(NAVSPACE);
} else if (RIGHT.equals(button)) {
button = extractButton(MENU_IME_ROTATE);
}
if (HOME.equals(button)) {
v = inflater.inflate(R.layout.home, parent, false);
} else if (BACK.equals(button)) {
v = inflater.inflate(R.layout.back, parent, false);
} else if (RECENT.equals(button)) {
v = inflater.inflate(R.layout.recent_apps, parent, false);
} else if (MENU_IME_ROTATE.equals(button)) {
v = inflater.inflate(R.layout.menu_ime, parent, false);
} else if (NAVSPACE.equals(button)) {
v = inflater.inflate(R.layout.nav_key_space, parent, false);
} else if (CLIPBOARD.equals(button)) {
v = inflater.inflate(R.layout.clipboard, parent, false);
} else if (CONTEXTUAL.equals(button)) {
v = inflater.inflate(R.layout.contextual, parent, false);
} else if (HOME_HANDLE.equals(button)) {
v = inflater.inflate(R.layout.home_handle, parent, false);
} else if (IME_SWITCHER.equals(button)) {
v = inflater.inflate(R.layout.ime_switcher, parent, false);
} else if (button.startsWith(KEY)) {
String uri = extractImage(button);
int code = extractKeycode(button);
v = inflater.inflate(R.layout.custom_key, parent, false);
((KeyButtonView) v).setCode(code);
if (uri != null) {
if (uri.contains(":")) {
((KeyButtonView) v).loadAsync(Icon.createWithContentUri(uri));
} else if (uri.contains("/")) {
int index = uri.indexOf('/');
String pkg = uri.substring(0, index);
int id = Integer.parseInt(uri.substring(index + 1));
((KeyButtonView) v).loadAsync(Icon.createWithResource(pkg, id));
}
}
}
return v;
}
KeyButtonView类是一个自定义视图,用于创建导航栏中的按键,如 Home、Back、Recent Apps等。这个类扩展了 ImageView 以提供额外的功能,如处理触摸事件、播放声音效果、发送按键事件等。
launcher.xml
<com.android.launcher3.LauncherRootView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:launcher="http://schemas.android.com/apk/res-auto"
android:id="@+id/launcher"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.android.launcher3.dragndrop.DragLayer
android:id="@+id/drag_layer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
android:importantForAccessibility="no">
<com.android.launcher3.views.AccessibilityActionsView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/home_screen"
/>
<!-- The workspace contains 5 screens of cells -->
<!-- DO NOT CHANGE THE ID -->
<com.android.launcher3.Workspace
android:id="@+id/workspace"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:theme="@style/HomeScreenElementTheme"
launcher:pageIndicator="@+id/page_indicator" />
<!-- DO NOT CHANGE THE ID -->
<include
android:id="@+id/hotseat"
layout="@layout/hotseat" />
<!-- Keep these behind the workspace so that they are not visible when
we go into AllApps -->
<com.android.launcher3.pageindicators.WorkspacePageIndicator
android:id="@+id/page_indicator"
android:layout_width="match_parent"
android:layout_height="@dimen/workspace_page_indicator_height"
android:layout_gravity="bottom|center_horizontal"
android:theme="@style/HomeScreenElementTheme" />
<include
android:id="@+id/drop_target_bar"
layout="@layout/drop_target_bar" />
<com.android.launcher3.views.ScrimView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/scrim_view"
android:background="@android:color/transparent" />
<include
android:id="@+id/overview_panel"
layout="@layout/overview_panel" />
<include
android:id="@+id/apps_view"
layout="@layout/all_apps"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.android.launcher3.dragndrop.DragLayer>
</com.android.launcher3.LauncherRootView>
overview_panel.xml
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<com.android.quickstep.views.LauncherRecentsView
android:id="@+id/overview_panel"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:accessibilityPaneTitle="@string/accessibility_recent_apps"
android:clipChildren="false"
android:clipToPadding="false"
android:visibility="invisible" />
<include
android:id="@+id/overview_actions_view"
layout="@layout/overview_actions_container" />
</merge>
overview_actions_container.xml
<com.android.quickstep.views.OverviewActionsView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:layout_gravity="center_horizontal|bottom">
<LinearLayout
android:id="@+id/action_buttons"
android:layout_width="match_parent"
android:layout_height="@dimen/overview_actions_height"
android:layout_gravity="bottom|center_horizontal"
android:orientation="horizontal">
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<Button
android:id="@+id/action_screenshot"
style="@style/OverviewActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableStart="@drawable/ic_screenshot"
android:text="@string/action_screenshot"
android:theme="@style/ThemeControlHighlightWorkspaceColor" />
<Space
android:id="@+id/action_split_space"
android:layout_width="@dimen/overview_actions_button_spacing"
android:layout_height="1dp"
android:visibility="gone" />
<Button
android:id="@+id/action_split"
style="@style/OverviewActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/action_split"
android:theme="@style/ThemeControlHighlightWorkspaceColor"
android:visibility="gone" />
<Space
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<Space
android:id="@+id/oav_three_button_space"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1"
android:visibility="gone" />
</LinearLayout>
</com.android.quickstep.views.OverviewActionsView>
RecentView.java
public RecentsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
BaseActivityInterface sizeStrategy) {
super(context, attrs, defStyleAttr);
setEnableFreeScroll(true);
mSizeStrategy = sizeStrategy;
mActivity = BaseActivity.fromContext(context);
mOrientationState = new RecentsOrientedState(
context, mSizeStrategy, this::animateRecentsRotationInPlace);
final int rotation = mActivity.getDisplay().getRotation();
mOrientationState.setRecentsRotation(rotation);
mScrollHapticMinGapMillis = getResources()
.getInteger(R.integer.recentsScrollHapticMinGapMillis);
mFastFlingVelocity = getResources()
.getDimensionPixelSize(R.dimen.recents_fast_fling_velocity);
mModel = RecentsModel.INSTANCE.get(context);
mIdp = InvariantDeviceProfile.INSTANCE.get(context);
mClearAllButton = (ClearAllButton) LayoutInflater.from(context)
.inflate(R.layout.overview_clear_all_button, this, false);
mClearAllButton.setOnClickListener(this::dismissAllTasks);
mTaskViewPool = new ViewPool<>(context, this, R.layout.task, 20 /* max size */,
10 /* initial size */);
mGroupedTaskViewPool = new ViewPool<>(context, this,
R.layout.task_grouped, 20 /* max size */, 10 /* initial size */);
mDesktopTaskViewPool = new ViewPool<>(context, this, R.layout.task_desktop,
5 /* max size */, 1 /* initial size */);
mIsRtl = mOrientationHandler.getRecentsRtlSetting(getResources());
setLayoutDirection(mIsRtl ? View.LAYOUT_DIRECTION_RTL : View.LAYOUT_DIRECTION_LTR);
mSplitPlaceholderSize = getResources().getDimensionPixelSize(
R.dimen.split_placeholder_size);
mSplitPlaceholderInset = getResources().getDimensionPixelSize(
R.dimen.split_placeholder_inset);
mSquaredTouchSlop = squaredTouchSlop(context);
mClampedScrollOffsetBound = getResources().getDimensionPixelSize(
R.dimen.transient_taskbar_clamped_offset_bound);
mEmptyIcon = context.getDrawable(R.drawable.ic_empty_recents);
mEmptyIcon.setCallback(this);
mEmptyMessage = context.getText(R.string.recents_empty_message);
mEmptyMessagePaint = new TextPaint();
mEmptyMessagePaint.setColor(Themes.getAttrColor(context, android.R.attr.textColorPrimary));
mEmptyMessagePaint.setTextSize(getResources()
.getDimension(R.dimen.recents_empty_message_text_size));
mEmptyMessagePaint.setTypeface(Typeface.create(Themes.getDefaultBodyFont(context),
Typeface.NORMAL));
mEmptyMessagePaint.setAntiAlias(true);
mEmptyMessagePadding = getResources()
.getDimensionPixelSize(R.dimen.recents_empty_message_text_padding);
setWillNotDraw(false);
updateEmptyMessage();
mOrientationHandler = mOrientationState.getOrientationHandler();
mTaskOverlayFactory = Overrides.getObject(
TaskOverlayFactory.class,
context.getApplicationContext(),
R.string.task_overlay_factory_class);
// Initialize quickstep specific cache params here, as this is constructed only once
mActivity.getViewCache().setCacheSize(R.layout.digital_wellbeing_toast, 5);
mTintingColor = getForegroundScrimDimColor(context);
// if multi-instance feature is enabled
if (FeatureFlags.ENABLE_MULTI_INSTANCE.get()) {
// invalidate the current list of tasks if filter changes with a fading in/out animation
mFilterState.setOnFilterUpdatedListener(() -> {
Animator animatorFade = mActivity.getStateManager().createStateElementAnimation(
RecentsAtomicAnimationFactory.INDEX_RECENTS_FADE_ANIM, 1f, 0f);
Animator animatorAppear = mActivity.getStateManager().createStateElementAnimation(
RecentsAtomicAnimationFactory.INDEX_RECENTS_FADE_ANIM, 0f, 1f);
animatorFade.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(@NonNull Animator animation) {
RecentsView.this.invalidateTaskList();
updateClearAllFunction();
reloadIfNeeded();
if (mPendingAnimation != null) {
mPendingAnimation.addEndListener(success -> {
animatorAppear.start();
});
} else {
animatorAppear.start();
}
}
});
animatorFade.start();
});
}
// make sure filter is turned off by default
mFilterState.setFilterBy(null);
}
当“清除所有”按钮被点击时,会调用 dismissAllTasks 方法来关闭所有任务。
通过创建任务视图池实例来加载布局。
task.xml
<!-- NOTE! don't add dimensions for margins / paddings / sizes that change per orientation to this
file, they need to be loaded at runtime. -->
<com.android.quickstep.views.TaskView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
xmlns:launcher="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:defaultFocusHighlightEnabled="false"
android:focusable="true"
launcher:borderColor="?androidprv:attr/materialColorOutline">
<com.android.quickstep.views.TaskThumbnailView
android:id="@+id/snapshot"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<!-- Filtering affects only alpha instead of the visibility since visibility can be altered
separately through RecentsView#resetFromSplitSelectionState() -->
<ImageView
android:id="@+id/show_windows"
android:layout_height="@dimen/recents_filter_icon_size"
android:layout_width="@dimen/recents_filter_icon_size"
android:layout_gravity="end"
android:alpha="0"
android:tint="@color/recents_filter_icon"
android:importantForAccessibility="no"
android:src="@drawable/ic_select_windows" />
<com.android.quickstep.views.IconView
android:id="@+id/icon"
android:layout_width="@dimen/task_thumbnail_icon_size"
android:layout_height="@dimen/task_thumbnail_icon_size"
android:focusable="false"
android:importantForAccessibility="no"/>
</com.android.quickstep.views.TaskView>
TaskView.java
protected void setIcon(IconView iconView, @Nullable Drawable icon) {
if (icon != null) {
iconView.setDrawable(icon);
iconView.setOnClickListener(v -> {
if (confirmSecondSplitSelectApp()) {
return;
}
showTaskMenu(iconView);
});
iconView.setOnLongClickListener(v -> {
requestDisallowInterceptTouchEvent(true);
return showTaskMenu(iconView);
});
} else {
iconView.setDrawable(null);
iconView.setOnClickListener(null);
iconView.setOnLongClickListener(null);
}
}
F ---------->内容补充说明
在 Android 11 中,最近任务( Recent Apps)的响应机制有所变化,它不再完全由 SystemUI 处理,而是部分集成到了 Launcher3 应用中。以下是 SystemUI 如何响应最近任务请求的概述:
初始化阶段:SystemUI 服务启动时,会初始化 Recents 组件。这通常涉及到 Recents.java 文件中的 Recents 类,它继承自 SystemUI。它实现了 CoreStartable 和 CommandQueue.Callbacks 接口。在 start() 方法中,会将 Recents 实例注册到 CommandQueue 中,并启动 RecentsImplementation 的实现类,如 OverviewProxyRecentsImpl。
处理用户交互:当用户通过手势或按键触发最近任务时,事件会传递到 PhoneWindowManager,然后调用 SystemUI 中的 Recents 组件的 toggleRecentApps() 方法。
OverviewProxyRecentsImpl:在 OverviewProxyRecentsImpl.java 中,toggleRecentApps() 方法会被调用。如果与 Launcher3 的服务连接成功,它会通过 OverviewProxyService 调用 Launcher3 中的 IOverviewProxy 接口的 onOverviewToggle() 方法。
Launcher3 中的 TouchInteractionService:在 Launcher3 应用中,TouchInteractionService.java 包含了 onOverviewToggle() 方法的实现。这个方法会触发最近任务界面的显示。
RecentsActivityCommand:最终,RecentsActivityCommand 会被执行,它负责启动 RecentsActivity,这是显示最近任务列表的界面。
显示最近任务:RecentsActivity 会显示最近任务列表,并且可能会涉及到 RecentsView 类,它负责显示任务的缩略图和提供用户交互。
在 Android 11 中,由于最近任务界面的实现部分被移动到了 Launcher3 应用中,因此如果替换了默认的 Launcher3,可能会导致最近任务功能失效。这是因为新的 Launcher 可能没有集成处理最近任务的逻辑。
OverviewProxyRecentsImpl.java 中的 toggleRecentApps 方法是处理最近任务(Recent Apps)切换的关键方法。当用户想要查看最近任务时,这个动作会被触发。
以下是 toggleRecentApps 方法的工作流程:
toggleRecentApps 方法首先会检查是否已经连接到了 Launcher3 的服务。如果是,它会将切换逻辑交给 Launcher3 处理。
如果连接到了 Launcher3 的服务,它会通过 OverviewProxyService 的 getProxy 方法获取到 IOverviewProxy 的实例。
然后,它会尝试调用 IOverviewProxy 实例的 onOverviewToggle 方法来切换最近任务的显示状态。
在 Launcher3 中,TouchInteractionService 会接收到这个切换请求,并调用 OverviewCommandHelper 的 onOverviewToggle 方法。
onOverviewToggle 方法会调用 ActivityManagerWrapper 的 closeSystemWindows 方法,传入 CLOSE_SYSTEM_WINDOWS_REASON_RECENTS 作为参数,这会导致所有前台的系统窗口关闭,为显示最近任务界面做准备。
最后,MAIN_EXECUTOR 会执行 RecentsActivityCommand,这是一个 Runnable 对象,负责启动 RecentsActivity,也就是最近任务的界面。
RecentsActivity 会设置视图并初始化 FallbackRecentsView,这是显示最近任务列表的主要视图。
当 RecentsView 附加到窗口时,它会注册任务堆栈变化的监听器,以便在任务发生变化时更新 UI。
TaskUtils.closeSystemWindowsAsync 是一个在 Android 系统中用于关闭系统窗口的异步方法。在 Android 11 版本中,最近任务(Recent Apps)的功能与 SystemUI 和 Launcher3 都有关联。当用户想要查看最近任务时,SystemUI 会通过 CommandQueue 的 showRecentApps 方法来触发这一动作。这个方法最终会调用到 IOverviewProxy 接口的 onOverviewShown 方法,而 Launcher3 中的 TouchInteractionService 服务的 TISBinder 对象实现了这个接口。
在 TISBinder 的 onOverviewShown 方法中,如果是因为用户按下最近任务键(通常是方块键)来触发的,那么会调用 TaskUtils.closeSystemWindowsAsync 方法来异步关闭系统窗口,并且传递一个原因参数 CLOSE_SYSTEM_WINDOWS_REASON_RECENTS。这样做是为了在显示最近任务视图之前,关闭其他所有前台活动窗口,确保用户界面的整洁和最近任务视图的突出显示。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)