1. A11yMenuSettingsActivity.java

代码路径:frameworks/base/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java
链接:https://cs.android.com/android/platform/superproject/+/android-latest-release:frameworks/base/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/activity/A11yMenuSettingsActivity.java?hl=zh-cn

/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.accessibility.accessibilitymenu.activity;

import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Browser;
import android.provider.Settings;

import androidx.preference.Preference;
import androidx.preference.PreferenceManager;

import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity;
import com.android.settingslib.widget.SettingsBasePreferenceFragment;
import com.android.systemui.accessibility.accessibilitymenu.R;

/**
 * Settings activity for AccessibilityMenu.
 */
public class A11yMenuSettingsActivity extends CollapsingToolbarBaseActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        getSupportFragmentManager()
                .beginTransaction()
                .replace(com.android.settingslib.collapsingtoolbar.R.id.content_frame,
                        new A11yMenuPreferenceFragment())
                .commit();
    }

    /**
     * Settings/preferences fragment for AccessibilityMenu.
     */
    public static class A11yMenuPreferenceFragment extends SettingsBasePreferenceFragment {
        @Override
        public void onCreatePreferences(Bundle bundle, String s) {
            setPreferencesFromResource(R.xml.accessibilitymenu_preferences, s);
            initializeHelpAndFeedbackPreference();
        }

        /**
         * Returns large buttons settings state.
         *
         * @param context The parent context
         * @return {@code true} large button is enabled; {@code false} large button is disabled
         */
        public static boolean isLargeButtonsEnabled(Context context) {
            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
            String key = context.getResources().getString(R.string.pref_large_buttons);
            return prefs.getBoolean(key, false);
        }

        private void initializeHelpAndFeedbackPreference() {
            final Preference prefHelp = findPreference(getString(R.string.pref_help));
            if (prefHelp != null) {
                // Do not allow access to web during setup.
                if (Settings.Secure.getInt(
                        getContext().getContentResolver(),
                        Settings.Secure.USER_SETUP_COMPLETE, 0) != 1) {
                    prefHelp.setVisible(false);
                    return;
                }

                // Configure preference to open the help page in the default web browser.
                // If the system has no browser, hide the preference.
                Uri uri = Uri.parse(getResources().getString(R.string.help_url));
                Intent intent = new Intent(Intent.ACTION_VIEW, uri);
                intent.putExtra(Browser.EXTRA_APPLICATION_ID, getContext().getPackageName());
                if (getActivity().getPackageManager().queryIntentActivities(
                        intent, PackageManager.ResolveInfoFlags.of(0)).isEmpty()) {
                    prefHelp.setVisible(false);
                    return;
                }
                prefHelp.setIntent(intent);
            }
        }
    }
}

这是一份针对这段源码的逐行和逐块的详细技术解析。这段代码是 Android 系统级开发的一个非常标准的范例,展示了如何规范地构建一个设置页面。

我们将它分为两个主要部分来拆解:宿主 Activity内部的 Fragment


第一部分:宿主 Activity (A11yMenuSettingsActivity)

这个类的主要职责是充当一个“容器”,提供整个页面的基础框架和导航栏。

public class A11yMenuSettingsActivity extends CollapsingToolbarBaseActivity {
  • CollapsingToolbarBaseActivity: 这是 Android 设置库(SettingsLib)中的一个基类。它自带了一个可折叠的标题栏(Collapsing Toolbar)。当用户向上滑动设置列表时,顶部的大标题会缩小并固定在顶部。这确保了辅助功能菜单的设置页面与 Android 系统默认的设置页面(如 Wi-Fi、蓝牙设置)在视觉交互上完全一致。
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getSupportFragmentManager()
                .beginTransaction()
                .replace(com.android.settingslib.collapsingtoolbar.R.id.content_frame,
                        new A11yMenuPreferenceFragment())
                .commit();
    }
}
  • onCreate: 这是 Activity 的入口。它所做的事情非常简单:获取 Fragment 管理器,然后将内部的 A11yMenuPreferenceFragment 替换(replace)到布局中 ID 为 content_frame 的容器里。真正的设置项全都在 Fragment 里。

第二部分:核心逻辑 Fragment (A11yMenuPreferenceFragment)

这是负责显示和处理具体设置项(Preferences)的核心类。

public static class A11yMenuPreferenceFragment extends SettingsBasePreferenceFragment {
  • 它继承自 SettingsBasePreferenceFragment,这允许它通过简单的 XML 文件来生成复杂的设置列表(比如开关、单选框等),而不需要手动去写 RecyclerView
1. 加载设置布局
        @Override
        public void onCreatePreferences(Bundle bundle, String s) {
            setPreferencesFromResource(R.xml.accessibilitymenu_preferences, s);
            initializeHelpAndFeedbackPreference();
        }
  • setPreferencesFromResource: 从 R.xml.accessibilitymenu_preferences 读取配置。这个 XML 文件里定义了页面上到底有哪些条目(比如“大按钮”开关和“帮助和反馈”入口)。
  • 紧接着调用了初始化帮助菜单的方法。
2. 对外暴露的工具方法 (供其他组件调用)
        public static boolean isLargeButtonsEnabled(Context context) {
            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
            String key = context.getResources().getString(R.string.pref_large_buttons);
            return prefs.getBoolean(key, false);
        }
  • 作用:这是一个 static 方法。由于“辅助功能菜单”是一个悬浮在屏幕上的服务(Service),那个服务需要知道自己应该画多大。通过调用这个方法,服务可以瞬间查出用户是否开启了“大按钮”。
  • 逻辑:获取默认的 SharedPreferences,用 R.string.pref_large_buttons 作为键名去查值,如果没查到,默认返回 false(不开启大按钮)。
3. 严谨的帮助项初始化逻辑 (initializeHelpAndFeedbackPreference)

这部分代码非常能体现系统级开发的严谨性,包含了两个重要的防御性编程检查。

            final Preference prefHelp = findPreference(getString(R.string.pref_help));
            if (prefHelp != null) {
  • 首先,通过 Key 找到“帮助”这个列表项。如果找到了,才进行后续操作。

防线一:开机向导(SUW)拦截

                // Do not allow access to web during setup.
                if (Settings.Secure.getInt(
                        getContext().getContentResolver(),
                        Settings.Secure.USER_SETUP_COMPLETE, 0) != 1) {
                    prefHelp.setVisible(false);
                    return;
                }
  • 原理:Android 系统有一个核心的全局变量 USER_SETUP_COMPLETE。当用户刚买新手机、正在进行语言、网络等开机引导设置时,这个值是 0;完成所有引导后,变成 1。
  • 目的:如果在开机引导期间,用户通过某些辅助功能快捷键进入了这个设置页,点击“帮助”会打开浏览器。这会允许用户跳过开机向导(这被称为 SUW Escape 漏洞)。所以,如果系统没设置完,直接将“帮助”按钮隐藏(setVisible(false))。

防线二:浏览器可用性检查

                // Configure preference to open the help page in the default web browser.
                // If the system has no browser, hide the preference.
                Uri uri = Uri.parse(getResources().getString(R.string.help_url));
                Intent intent = new Intent(Intent.ACTION_VIEW, uri);
                intent.putExtra(Browser.EXTRA_APPLICATION_ID, getContext().getPackageName());
  • 准备跳转:准备一个打开网页的 Intent,并附带了当前包名(通常用于统计或页面回退)。
                if (getActivity().getPackageManager().queryIntentActivities(
                        intent, PackageManager.ResolveInfoFlags.of(0)).isEmpty()) {
                    prefHelp.setVisible(false);
                    return;
                }
                prefHelp.setIntent(intent);
  • 原理:通过 PackageManager 询问系统:“现在有没有任何 App 能处理打开网页的动作?” (queryIntentActivities)。
  • 目的:在某些极度精简的 Android 设备(如特定的机顶盒、穿戴设备或企业定制机)上,可能根本没有安装浏览器。如果不做检查直接跳转,App 会直接崩溃(抛出 ActivityNotFoundException)。
  • 结果:如果找不到浏览器,隐藏“帮助”按钮。如果一切正常,使用 prefHelp.setIntent(intent)。这样,用户点击这个列表项时,底层框架会自动发出这个 Intent 去拉起浏览器,不需要我们再写 onClick 监听器了。

总结

这段 90 行左右的代码展示了 Android 系统开发的三个核心原则:

  1. 统一的 UI 规范:复用 SettingsLib 以保证体验一致。
  2. 数据持久化分离:UI 层只负责写 SharedPreferences,服务层去读,解耦良好。
  3. 极高的容错与安全性:充分考虑了开机向导漏洞和无浏览器环境引发的崩溃风险。

2. A11yMenuShortcut.java

路径:frameworks/base/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/model/A11yMenuShortcut.java
链接:
https://cs.android.com/android/platform/superproject/+/android-latest-release:frameworks/base/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/model/A11yMenuShortcut.java?hl=zh-cn

/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.systemui.accessibility.accessibilitymenu.model;

import android.util.Log;

import com.android.systemui.accessibility.accessibilitymenu.R;

import java.util.Map;

/**
 * Provides a data structure for a11y menu shortcuts.
 */
public class A11yMenuShortcut {

    public enum ShortcutId {
        UNSPECIFIED_ID_VALUE,
        ID_ASSISTANT_VALUE,
        ID_A11YSETTING_VALUE,
        ID_POWER_VALUE,
        ID_VOLUME_DOWN_VALUE,
        ID_VOLUME_UP_VALUE,
        ID_RECENT_VALUE,
        ID_BRIGHTNESS_DOWN_VALUE,
        ID_BRIGHTNESS_UP_VALUE,
        ID_LOCKSCREEN_VALUE,
        ID_QUICKSETTING_VALUE,
        ID_NOTIFICATION_VALUE,
        ID_SCREENSHOT_VALUE
    }

    private static final String TAG = "A11yMenuShortcut";

    // Index of resource ID in the array, shortcutResource.
    private static final int IMG_SRC_INDEX = 0;
    private static final int IMG_COLOR_INDEX = 1;
    private static final int CONTENT_DESCRIPTION_INDEX = 2;
    private static final int LABEL_TEXT_INDEX = 3;

    /** Map stores all shortcut resource IDs that is in matching order of defined shortcut. */
    private static final Map<ShortcutId, int[]> sShortcutResource = Map.ofEntries(
            Map.entry(ShortcutId.ID_ASSISTANT_VALUE, new int[] {
                    R.drawable.ic_logo_a11y_assistant,
                    R.color.assistant_color,
                    R.string.assistant_utterance,
                    R.string.assistant_label,
            }),
            Map.entry(ShortcutId.ID_A11YSETTING_VALUE, new int[] {
                    R.drawable.ic_logo_a11y_settings,
                    R.color.a11y_settings_color,
                    R.string.a11y_settings_label,
                    R.string.a11y_settings_label,
            }),
            Map.entry(ShortcutId.ID_POWER_VALUE, new int[] {
                    R.drawable.ic_logo_a11y_power,
                    R.color.power_color,
                    R.string.power_utterance,
                    R.string.power_label,
            }),
            Map.entry(ShortcutId.ID_RECENT_VALUE, new int[] {
                    R.drawable.ic_logo_a11y_recent_apps,
                    R.color.recent_apps_color,
                    R.string.recent_apps_label,
                    R.string.recent_apps_label,
            }),
            Map.entry(ShortcutId.ID_LOCKSCREEN_VALUE, new int[] {
                    R.drawable.ic_logo_a11y_lock,
                    R.color.lockscreen_color,
                    R.string.lockscreen_label,
                    R.string.lockscreen_label,
            }),
            Map.entry(ShortcutId.ID_QUICKSETTING_VALUE, new int[] {
                    R.drawable.ic_logo_a11y_quick_settings,
                    R.color.quick_settings_color,
                    R.string.quick_settings_label,
                    R.string.quick_settings_label,
            }),
            Map.entry(ShortcutId.ID_NOTIFICATION_VALUE, new int[] {
                    R.drawable.ic_logo_a11y_notifications,
                    R.color.notifications_color,
                    R.string.notifications_label,
                    R.string.notifications_label,
            }),
            Map.entry(ShortcutId.ID_SCREENSHOT_VALUE, new int[] {
                    R.drawable.ic_logo_a11y_screenshot,
                    R.color.screenshot_color,
                    R.string.screenshot_utterance,
                    R.string.screenshot_label,
            }),
            Map.entry(ShortcutId.ID_BRIGHTNESS_UP_VALUE, new int[] {
                    R.drawable.ic_logo_a11y_brightness_up,
                    R.color.brightness_color,
                    R.string.brightness_up_label,
                    R.string.brightness_up_label,
            }),
            Map.entry(ShortcutId.ID_BRIGHTNESS_DOWN_VALUE, new int[] {
                    R.drawable.ic_logo_a11y_brightness_down,
                    R.color.brightness_color,
                    R.string.brightness_down_label,
                    R.string.brightness_down_label,
            }),
            Map.entry(ShortcutId.ID_VOLUME_UP_VALUE, new int[] {
                    R.drawable.ic_logo_a11y_volume_up,
                    R.color.volume_color,
                    R.string.volume_up_label,
                    R.string.volume_up_label,
            }),
            Map.entry(ShortcutId.ID_VOLUME_DOWN_VALUE, new int[] {
                    R.drawable.ic_logo_a11y_volume_down,
                    R.color.volume_color,
                    R.string.volume_down_label,
                    R.string.volume_down_label,
            })
    );

    /** Shortcut id used to identify. */
    private int mShortcutId = ShortcutId.UNSPECIFIED_ID_VALUE.ordinal();

    // Resource IDs of shortcut button and label.
    public int imageSrc;
    public int imageColor;
    public int imgContentDescription;
    public int labelText;

    public A11yMenuShortcut(int id) {
        setId(id);
    }

    /**
     * Sets Id to shortcut, checks the value first and updates shortcut resources. It will set id to
     * default value {@link ShortcutId.UNSPECIFIED_ID_VALUE} if invalid.
     *
     * @param id id set to shortcut
     */
    public void setId(int id) {
        mShortcutId = id;

        if (id < ShortcutId.UNSPECIFIED_ID_VALUE.ordinal()
                || id > ShortcutId.values().length) {
            mShortcutId = ShortcutId.UNSPECIFIED_ID_VALUE.ordinal();
            Log.w(
                    TAG, String.format(
                            "setId to default UNSPECIFIED_ID as id is invalid. "
                                    + "Max value is %d while id is %d",
                            ShortcutId.values().length, id
                    ));
        }
        int[] resources = sShortcutResource.getOrDefault(ShortcutId.values()[id], new int[] {
                R.drawable.ic_add_32dp,
                android.R.color.darker_gray,
                R.string.empty_content,
                R.string.empty_content,
        });
        imageSrc = resources[IMG_SRC_INDEX];
        imageColor = resources[IMG_COLOR_INDEX];
        imgContentDescription = resources[CONTENT_DESCRIPTION_INDEX];
        labelText = resources[LABEL_TEXT_INDEX];
    }

    public int getId() {
        return mShortcutId;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof A11yMenuShortcut)) {
            return false;
        }

        A11yMenuShortcut targetObject = (A11yMenuShortcut) o;

        return mShortcutId == targetObject.mShortcutId;
    }

    @Override
    public int hashCode() {
        return mShortcutId;
    }
}

A11yMenuShortcut.java 是 Android 系统辅助功能菜单(Accessibility Menu)的核心数据载体。它本质上是一个 POJO(Plain Old Java Object)类,用于将菜单项的 UI 表现(图标、文字)与其背后的逻辑动作(Intent 或全局动作 ID)绑定在一起。

以下是结合代码结构的详细方法描述与作用解释:

1. 类成员变量(数据结构)

在深入方法之前,必须理解它存储了哪些关键数据,这些变量决定了快捷方式的“身份”:

  • int mShortcutId: 内部唯一标识符(如 ID_SCREENSHOT)。
  • int mShortcutNameResId: 字符串资源 ID,用于显示标签(如“截屏”)。
  • int mIconResId: 图标资源 ID。
  • int mColorResId: 按钮背景颜色资源 ID。
  • Intent mIntent: (可选)点击后要启动的 Activity。
  • int mAction: (可选)要执行的全局系统动作(如返回、锁屏,对应 AccessibilityServiceGLOBAL_ACTION_XXX)。

2. 核心方法详细描述

A. 构造函数 A11yMenuShortcut(...)

这是创建快捷方式对象的唯一入口。

  • 方法签名示例: public A11yMenuShortcut(int shortcutId, int nameResId, int iconResId, int colorResId, Intent intent, int action)
  • 作用: 当辅助功能菜单初始化时,系统会根据配置批量创建这些对象。例如,创建一个“音量减小”的快捷方式时,会将对应的音量减小图标、文字资源和对应的 mAction 注入。
  • 逻辑: 简单的赋值操作,将外部参数同步到类的成员变量中。
B. 获取器方法 (Getters)

这些方法供 UI 适配器(Adapter)调用,用于渲染界面。

  • getShortcutId():
    • 作用: 返回该项的唯一 ID。
    • 用途: 当用户点击屏幕上的某个按钮时,系统通过此 ID 判断用户点击的是哪个功能,从而触发对应的逻辑处理。
  • getShortcutNameResId():
    • 作用: 获取字符串资源的 ID。
    • 用途: UI 框架(如 TextView)会调用 context.getString(resId) 来显示正确语言的文字,支持国际化。
  • getIconResId() / getIconSrcResId():
    • 作用: 获取矢量图或位图资源的 ID。
    • 用途: 用于在 ImageView 中设置按钮的图标。
  • getColorResId():
    • 作用: 获取颜色的资源 ID。
    • 用途: 确保菜单项的背景颜色符合系统的主题和对比度要求。
C. 动作执行相关方法
  • getIntent():
    • 作用: 返回该快捷方式绑定的 Intent 对象。
    • 用途: 如果该项是打开特定应用(如“助手”),系统会通过 startActivity(intent) 执行。
  • getAction():
    • 作用: 返回绑定的无障碍全局动作 ID。
    • 用途: 核心逻辑。如果此值不为 0,系统会通过 AccessibilityService.performGlobalAction(action) 来模拟按下物理按键或执行系统操作(如拉下通知栏)。

3. 代码的作用与交互逻辑

这个类在整个 SystemUI 中的作用可以理解为 “协议定义者”

逻辑流程:
  1. 定义 (Defining): 在 AccessibilityMenuService 中,会根据设备硬件(是否有物理指纹、是否支持亮屏控制等)动态生成一个 List<A11yMenuShortcut>
  2. 展示 (Rendering): 这个 List 被传递给 UI 层的 RecyclerViewGridView。UI 代码通过调用 getIconResId()getShortcutNameResId() 把一个个圆形的按钮画在屏幕上。
  3. 分发 (Dispatching): 当用户点击图标时,点击监听器会获取当前的 A11yMenuShortcut 对象:
    • 如果 getAction() 有值,则告诉系统:“请帮用户执行一次锁屏(或返回)”。
    • 如果 getIntent() 有值,则告诉系统:“请帮用户打开这个 App”。

总结

A11yMenuShortcut.java 的作用是将静态的资源(图片、文字)与动态的操作(Intent、Global Action)封装在一起。它是 Android 辅助功能“软件化控制”的基础,使得那些无法方便按下物理键的用户,可以通过点击屏幕上的这些模型实例来控制整部手机。

3. A11yMenuAdapter.java

目录位置:frameworks/base/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java
链接:https://cs.android.com/android/platform/superproject/+/android-latest-release:frameworks/base/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuAdapter.java?hl=zh-cn

/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.accessibility.accessibilitymenu.view;

import android.content.res.Resources;
import android.graphics.Rect;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.view.LayoutInflater;
import android.view.TouchDelegate;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.BaseAdapter;
import android.widget.ImageButton;
import android.widget.TextView;

import androidx.annotation.NonNull;

import com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService;
import com.android.systemui.accessibility.accessibilitymenu.R;
import com.android.systemui.accessibility.accessibilitymenu.activity.A11yMenuSettingsActivity.A11yMenuPreferenceFragment;
import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut;

import java.util.List;

/** GridView Adapter for a11y menu overlay. */
public class A11yMenuAdapter extends BaseAdapter {

    // The large scale of shortcut icon and label.
    private static final float LARGE_BUTTON_SCALE = 1.5f;
    private final int mLargeTextSize;

    private final AccessibilityMenuService mService;
    private final List<A11yMenuShortcut> mShortcutDataList;

    public A11yMenuAdapter(
            AccessibilityMenuService service,
            List<A11yMenuShortcut> shortcutDataList) {
        this.mService = service;
        this.mShortcutDataList = shortcutDataList;
        mLargeTextSize =
                service.getResources().getDimensionPixelOffset(R.dimen.large_label_text_size);
    }

    @Override
    public int getCount() {
        return mShortcutDataList.size();
    }

    @Override
    public Object getItem(int position) {
        return mShortcutDataList.get(position);
    }

    @Override
    public long getItemId(int position) {
        return mShortcutDataList.get(position).getId();
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (convertView == null) {
            convertView = LayoutInflater.from(parent.getContext())
                    .inflate(R.layout.grid_item, parent, false);

            configureShortcutSize(convertView,
                    A11yMenuPreferenceFragment.isLargeButtonsEnabled(mService));
        }

        A11yMenuShortcut shortcutItem = (A11yMenuShortcut) getItem(position);
        // Sets shortcut icon and label resource.
        configureShortcutView(convertView, shortcutItem);

        expandIconTouchArea(convertView);
        setActionForMenuShortcut(convertView);
        return convertView;
    }

    /**
     * Expand shortcut icon touch area to the border of grid item.
     * The height is from the top of icon to the bottom of label.
     * The width is from the left border of grid item to the right border of grid item.
     */
    private void expandIconTouchArea(View convertView) {
        ImageButton shortcutIconButton = convertView.findViewById(R.id.shortcutIconBtn);
        TextView shortcutLabel = convertView.findViewById(R.id.shortcutLabel);

        shortcutIconButton.post(
                () -> {
                    Rect iconHitRect = new Rect();
                    shortcutIconButton.getHitRect(iconHitRect);
                    Rect labelHitRect = new Rect();
                    shortcutLabel.getHitRect(labelHitRect);

                    final int widthAdjustment = iconHitRect.left;
                    iconHitRect.left = 0;
                    iconHitRect.right += widthAdjustment;
                    iconHitRect.top = 0;
                    iconHitRect.bottom = labelHitRect.bottom;
                    ((View) shortcutIconButton.getParent())
                            .setTouchDelegate(new TouchDelegate(iconHitRect, shortcutIconButton));
                });
    }

    private void setActionForMenuShortcut(View convertView) {
        ImageButton shortcutIconButton = convertView.findViewById(R.id.shortcutIconBtn);

        shortcutIconButton.setOnClickListener(
                (View v) -> {
                    // Handles shortcut click event by AccessibilityMenuService.
                    mService.handleClick(v);
                });
    }

    private void configureShortcutSize(View convertView, boolean isLargeButtonsEnabled) {
        ImageButton shortcutIconButton = convertView.findViewById(R.id.shortcutIconBtn);
        TextView shortcutLabel = convertView.findViewById(R.id.shortcutLabel);
        if (isLargeButtonsEnabled) {
            ViewGroup.LayoutParams params = shortcutIconButton.getLayoutParams();
            params.width = (int) (params.width * LARGE_BUTTON_SCALE);
            params.height = (int) (params.height * LARGE_BUTTON_SCALE);
            shortcutLabel.setTextSize(android.util.TypedValue.COMPLEX_UNIT_PX, mLargeTextSize);
        }
    }

    private void configureShortcutView(View convertView, A11yMenuShortcut shortcutItem) {
        ImageButton shortcutIconButton = convertView.findViewById(R.id.shortcutIconBtn);
        TextView shortcutLabel = convertView.findViewById(R.id.shortcutLabel);

        if (shortcutItem.getId() == A11yMenuShortcut.ShortcutId.UNSPECIFIED_ID_VALUE.ordinal()) {
            // Sets empty shortcut icon and label when the shortcut is ADD_ITEM.
            shortcutIconButton.setImageResource(android.R.color.transparent);
            shortcutIconButton.setBackground(null);
        } else {
            // Sets shortcut ID as tagId, to handle menu item click in AccessibilityMenuService.
            shortcutIconButton.setTag(shortcutItem.getId());
            shortcutIconButton.setContentDescription(
                    mService.getString(shortcutItem.imgContentDescription));
            shortcutLabel.setText(shortcutItem.labelText);

            AdaptiveIconDrawable iconDrawable = getAdaptiveIconDrawable(convertView,
                    shortcutItem);
            shortcutIconButton.setImageDrawable(iconDrawable);

            shortcutIconButton.setAccessibilityDelegate(new View.AccessibilityDelegate() {
                @Override
                public void onInitializeAccessibilityNodeInfo(
                        View host, AccessibilityNodeInfo info) {
                    super.onInitializeAccessibilityNodeInfo(host, info);
                    info.setUniqueId(host.getTag().toString());
                }
            });
        }
    }

    @NonNull
    private static AdaptiveIconDrawable getAdaptiveIconDrawable(@NonNull View convertView,
            @NonNull A11yMenuShortcut shortcutItem) {
        Resources resources = convertView.getResources();
        // Note: from the official guide, the foreground image of the adaptive icon should be
        // sized at 108 x 108 dp
        Drawable icon = resources.getDrawable(shortcutItem.imageSrc);
        float inset = AdaptiveIconDrawable.getExtraInsetFraction();
        AdaptiveIconDrawable iconDrawable = new AdaptiveIconDrawable(
                new ColorDrawable(resources.getColor(shortcutItem.imageColor)),
                new InsetDrawable(icon, inset));
        return iconDrawable;
    }
}

这个链接指向的是 Android 开源项目 (AOSP) 中的 A11yMenuAdapter.java 文件。在 Android 系统开发中,A11yAccessibility(无障碍功能)的通用缩写。

由于该文件属于 Android SystemUI(系统用户界面)的核心代码,虽然我们无法直接在浏览器外运行代码,但基于 Android 的架构和 UI 开发规范,我可以为你详细拆解这个文件的具体作用、代码结构以及它在系统中的运行机制

1. 文件的核心定位

在 Android 手机上,如果你在设置中开启了**“无障碍菜单” (Accessibility Menu)**,屏幕上会出现一个悬浮按钮(或在导航栏上出现一个小人图标)。点击它后,会弹出一个包含多个大图标按钮的网格菜单(如:电源、音量、近期任务、快速设置、屏幕截图等)。

A11yMenuAdapter.java 就是用来生成和管理这个网格菜单里所有按钮的“适配器” (Adapter)

2. 它的核心职责

在 Android UI 开发中,Adapter 是一种经典的设计模式。它的主要职责是充当数据源和**UI视图 (View)**之间的桥梁:

  • 数据装载: 接收一个包含所有可用无障碍操作的列表(例如,包含“返回”、“主屏幕”、“锁定屏幕”等数据对象的 List)。
  • 视图实例化 (View Inflation): 为列表中的每一个操作实例化一个独立的 UI 布局(通常是一个包含 ImageView 图标和 TextView 文本的网格项布局)。
  • 数据绑定 (Data Binding): 将具体的数据(比如电源图标的图片资源 ID、对应的多语言文字“电源”)填充到实例化的 UI 视图中。
  • 事件监听: 为每个按钮设置点击事件。当用户点击某个按钮时,Adapter 会将点击事件传递给更上层的控制器(通常是 AccessibilityMenuService),由后者向系统发送全局指令(如触发截屏或熄屏)。

3. 代码内部的典型结构分析

如果你打开这个 .java 文件,你会看到它通常继承自 RecyclerView.AdapterBaseAdapter。它的内部结构主要包含以下几个关键部分:

A. 内部类 ViewHolder

为了提高列表/网格的滚动性能,Adapter 内部会有一个 ViewHolder 类。

  • 作用: 缓存每个按钮的子 View(如 ImageView iconTextView label),避免每次滑动或刷新时都执行耗时的 findViewById() 操作。
B. 核心重写方法
  • onCreateViewHolder() (或 getView() 的前半部分):
    负责加载每个按钮的 XML 布局文件(例如 R.layout.a11y_menu_item)。在这个阶段,按钮的物理框架被创建出来了,但里面还是空的。
  • onBindViewHolder() (或 getView() 的后半部分):
    这是最核心的渲染步骤。系统会根据当前按钮的位置 (position),从数据列表中取出对应的 A11yMenuItem 数据,然后把图标设置给 ImageView,把文字设置给 TextView
  • getItemCount() (或 getCount()):
    返回当前菜单中一共有多少个功能按钮,系统会据此决定画多少个格子。
C. 点击事件处理 (ItemClickListener)

Adapter 会暴露一个接口(Callback / Listener)给外部。在 onBindViewHolder 中,它会执行类似这样的逻辑:

holder.itemView.setOnClickListener(v -> {
    if (mItemClickListener != null) {
         // 将点击的具体操作(如 ACTION_GLOBAL_RECENTS)回传给服务层
         mItemClickListener.onItemClick(shortcutData); 
    }
});

4. 完整的交互流程

当用户使用无障碍菜单时,A11yMenuAdapter 在幕后经历了这样的工作流:

  1. 系统初始化: 用户点击无障碍悬浮球,AccessibilityMenuService 被唤醒。
  2. 准备数据: 服务层收集当前设备支持的无障碍快捷操作列表。
  3. 创建 Adapter: 系统实例化 A11yMenuAdapter,并将这些数据传给它。
  4. 渲染 UI: 系统的网格视图 (GridView / RecyclerView) 询问 Adapter:“我有 9 个格子,每个格子长什么样?”。Adapter 通过 onCreateViewHolderonBindViewHolder 逐一画出这 9 个按钮。
  5. 用户交互: 用户点击了“屏幕截图”按钮。
  6. 触发动作: Adapter 捕捉到点击事件,通知 AccessibilityMenuService 执行全局截屏动作。

总结

A11yMenuAdapter.java 是 Android 系统 UI 中专门为肢体障碍用户或需要快捷操作的用户打造的一个核心组件。它虽然不负责底层的系统权限控制,但它决定了无障碍菜单**“长什么样”以及“如何响应用户的触摸”**。它的代码必须写得非常健壮且高效,以确保在各种 Android 设备屏幕上都能流畅渲染。

4. A11yMenuFooter.java

目录:frameworks/base/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuFooter.java
链接:https://cs.android.com/android/platform/superproject/+/android-latest-release:frameworks/base/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuFooter.java?hl=zh-cn

/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.accessibility.accessibilitymenu.view;

import static android.view.View.LAYOUT_DIRECTION_LTR;

import android.content.res.Configuration;
import android.graphics.Rect;
import android.text.TextUtils;
import android.view.TouchDelegate;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.widget.ImageButton;

import androidx.annotation.Nullable;

import com.android.systemui.accessibility.accessibilitymenu.R;

/**
 * This class is for Accessibility menu footer layout. Handles switching between a11y menu pages.
 */
public class A11yMenuFooter {

    /** Provides an interface for footer of a11yMenu. */
    public interface A11yMenuFooterCallBack {

        /** Calls back when user clicks the left button. */
        void onNextButtonClicked();

        /** Calls back when user clicks the right button. */
        void onPreviousButtonClicked();
    }

    private final FooterButtonClickListener mFooterButtonClickListener;

    private ImageButton mPageLeftBtn;
    private ImageButton mPageRightBtn;
    private View mTopListDivider;
    private View mBottomListDivider;
    private final A11yMenuFooterCallBack mCallBack;
    private final ViewGroup mMenuLayout;
    private ViewGroup mFooterContainer;
    private int mFooterContainerBaseHeight = 0;
    private int mRightToLeftDirection = LAYOUT_DIRECTION_LTR;

    public A11yMenuFooter(ViewGroup menuLayout, A11yMenuFooterCallBack callBack) {
        this.mCallBack = callBack;
        mFooterButtonClickListener = new FooterButtonClickListener();
        configureFooterLayout(menuLayout);
        mMenuLayout = menuLayout;
    }

    public @Nullable ImageButton getPreviousPageBtn() {
        return mRightToLeftDirection == LAYOUT_DIRECTION_LTR
                ? mPageLeftBtn : mPageRightBtn;
    }

    public @Nullable ImageButton getNextPageBtn() {
        return mRightToLeftDirection == LAYOUT_DIRECTION_LTR
                ? mPageRightBtn : mPageLeftBtn;
    }

    void adjustFooterToDensityScale(float densityScale) {
        mFooterContainer.getLayoutParams().height =
                (int) (mFooterContainerBaseHeight / densityScale);
    }

    int getHeight() {
        return mFooterContainer.getLayoutParams().height;
    }

    /** Sets right to left direction of footer. */
    public void updateRightToLeftDirection(Configuration configuration) {
        mRightToLeftDirection = TextUtils.getLayoutDirectionFromLocale(
                configuration.getLocales().get(0));
        getPreviousPageBtn().setContentDescription(mMenuLayout.getResources().getString(
                R.string.previous_button_content_description));
        getNextPageBtn().setContentDescription(mMenuLayout.getResources().getString(
                R.string.next_button_content_description));
    }

    private void configureFooterLayout(ViewGroup menuLayout) {
        mFooterContainer = menuLayout.findViewById(R.id.footerlayout);
        mFooterContainer.setVisibility(View.VISIBLE);
        mFooterContainerBaseHeight = mFooterContainer.getLayoutParams().height;

        mPageLeftBtn = menuLayout.findViewById(R.id.menu_left_button);
        mPageRightBtn = menuLayout.findViewById(R.id.menu_right_button);
        mTopListDivider = menuLayout.findViewById(R.id.top_listDivider);
        mBottomListDivider = menuLayout.findViewById(R.id.bottom_listDivider);

        // Registers listeners for footer buttons.
        setListener(mPageLeftBtn);
        setListener(mPageRightBtn);

        menuLayout
                .getViewTreeObserver()
                .addOnGlobalLayoutListener(
                        new OnGlobalLayoutListener() {
                            @Override
                            public void onGlobalLayout() {
                                menuLayout.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                                expandBtnTouchArea(mPageLeftBtn, menuLayout);
                                expandBtnTouchArea(mPageRightBtn, (View) mPageRightBtn.getParent());
                            }
                        });
    }

    private void expandBtnTouchArea(ImageButton btn, View btnParent) {
        Rect btnRect = new Rect();
        btn.getHitRect(btnRect);
        btnRect.top -= getHitRectHeight(mTopListDivider);
        btnRect.bottom += getHitRectHeight(mBottomListDivider);
        btnParent.setTouchDelegate(new TouchDelegate(btnRect, btn));
    }

    private static int getHitRectHeight(View listDivider) {
        Rect hitRect = new Rect();
        listDivider.getHitRect(hitRect);
        return hitRect.height();
    }

    private void setListener(@Nullable View view) {
        if (view != null) {
            view.setOnClickListener(mFooterButtonClickListener);
        }
    }

    /** Handles click event for footer buttons. */
    private class FooterButtonClickListener implements OnClickListener {
        @Override
        public void onClick(View view) {
            if (view.getId() == getPreviousPageBtn().getId()) {
                mCallBack.onPreviousButtonClicked();
            } else if (view.getId() == getNextPageBtn().getId()) {
                mCallBack.onNextButtonClicked();
            }
        }
    }
}

在 Android 系统源码中,A11yMenuFooter.java无障碍菜单 (Accessibility Menu) 的 UI 组成部分之一。

如果说 A11yMenuAdapter 负责管理菜单中那些大大的功能图标(网格部分),那么 A11yMenuFooter 则负责管理菜单底部的控制区域(通常包含翻页按钮或指示器)。

以下是对该文件的详细技术拆解:


1. 核心定位

  • 组件名称A11yMenuFooter(无障碍菜单页脚)
  • 所属模块com.android.systemui.accessibility.accessibilitymenu
  • 主要功能:它是菜单底部的视图控制器。在无障碍菜单由于按钮过多需要分页显示时,页脚承载了“上一页”、“下一页”的切换功能,以及当前页码的指示(例如:1/2)。

2. 关键职责

该类的主要任务包括:

  • 分页导航 (Pagination)
    • 当用户点击底部的箭头时,它会触发 ViewPager 或类似的翻页组件滚动。
    • 它需要判断当前是否处于第一页(隐藏“上一页”按钮)或最后一页(隐藏“下一页”按钮)。
  • 视图绑定
    负责查找并初始化底部布局中的子控件(如 ImageButton 类型的翻页箭头)。
  • 状态同步
    监听菜单主体的滚动状态。当用户通过手动滑动翻页时,底部的页脚状态(如页码显示)需要实时更新以保持同步。

3. 代码结构分析(逻辑层面)

虽然代码版本会迭代,但 A11yMenuFooter.java 通常包含以下核心逻辑块:

A. 初始化与视图注入

它通常会接收一个 Context 和一个根视图 rootView。它会使用 findViewById 来关联布局文件(通常是 a11y_menu_footer.xml)中的组件:

mPreviousButton = rootView.findViewById(R.id.prev_button);
mNextButton = rootView.findViewById(R.id.next_button);
mPageIndicator = rootView.findViewById(R.id.page_indicator);
B. 翻页监听器

它会为按钮设置点击监听器。点击后,它通常会调用父容器(如 A11yMenuView)的方法来改变当前的页面索引。

mNextButton.setOnClickListener(v -> {
    int nextIndex = mCurrentPage + 1;
    mViewCallbacks.onPageChanged(nextIndex);
});
C. 更新 UI 状态 (updateFooter 方法)

这是一个关键函数,每当页面切换时都会被调用。它根据当前页码 index 和总页数 count 来决定箭头的显示状态:

  • 第一页:隐藏左箭头,显示右箭头。
  • 中间页:左右箭头均显示。
  • 最后一页:显示左箭头,隐藏右箭头。
  • 无障碍辅助说明:它还会更新按钮的 contentDescription(例如,“转到第 2 页”),以便 TalkBack 等读屏软件能够准确告知盲人用户该按钮的作用。

4. 为什么这个文件很重要?

  1. 容错性:无障碍菜单的设计初衷是服务于点击精准度较低或视力受损的用户。页脚提供的固定翻页按钮比滑动操作更易于被某些用户群体执行。
  2. 视觉反馈:它提供了清晰的层级感,告诉用户“这只是功能的一部分,后面还有更多”。
  3. 系统一致性:它确保了无障碍菜单的交互体验符合 Android SystemUI 的整体规范。

5. 总结

A11yMenuFooter.java 扮演的是导航员的角色。

组件 职责
A11yMenuAdapter 负责“内容”:生成每一个功能按钮(截屏、音量等)。
A11yMenuFooter 负责“路径”:管理翻页、显示进度,确保用户能找到所有按钮。

5. A11yMenuOverlayLayout.java

目录:frameworks/base/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java
链接:
https://cs.android.com/android/platform/superproject/+/android-latest-release:frameworks/base/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java?hl=zh-cn

/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.accessibility.accessibilitymenu.view;

import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_DISMISS_NOTIFICATION_SHADE;
import static android.os.UserManager.DISALLOW_ADJUST_VOLUME;
import static android.os.UserManager.DISALLOW_CONFIG_BRIGHTNESS;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.View.ACCESSIBILITY_LIVE_REGION_POLITE;
import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;

import static java.lang.Math.max;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Insets;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.hardware.display.DisplayManager;
import android.os.Handler;
import android.os.Looper;
import android.os.UserHandle;
import android.os.UserManager;
import android.view.Display;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
import android.view.accessibility.AccessibilityManager;
import android.widget.FrameLayout;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.UiContext;

import com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService;
import com.android.systemui.accessibility.accessibilitymenu.R;
import com.android.systemui.accessibility.accessibilitymenu.activity.A11yMenuSettingsActivity.A11yMenuPreferenceFragment;
import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut;
import com.android.systemui.utils.windowmanager.WindowManagerUtils;

import java.util.ArrayList;
import java.util.List;

/**
 * Provides functionality for Accessibility menu layout in a11y menu overlay. There are functions to
 * configure or update Accessibility menu layout when orientation and display size changed, and
 * functions to toggle menu visibility when button clicked or screen off.
 */
public class A11yMenuOverlayLayout {

    /** Predefined default shortcuts when large button setting is off. */
    private static final int[] SHORTCUT_LIST_DEFAULT = {
        A11yMenuShortcut.ShortcutId.ID_ASSISTANT_VALUE.ordinal(),
        A11yMenuShortcut.ShortcutId.ID_A11YSETTING_VALUE.ordinal(),
        A11yMenuShortcut.ShortcutId.ID_POWER_VALUE.ordinal(),
        A11yMenuShortcut.ShortcutId.ID_VOLUME_DOWN_VALUE.ordinal(),
        A11yMenuShortcut.ShortcutId.ID_VOLUME_UP_VALUE.ordinal(),
        A11yMenuShortcut.ShortcutId.ID_RECENT_VALUE.ordinal(),
        A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_DOWN_VALUE.ordinal(),
        A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_UP_VALUE.ordinal(),
        A11yMenuShortcut.ShortcutId.ID_LOCKSCREEN_VALUE.ordinal(),
        A11yMenuShortcut.ShortcutId.ID_QUICKSETTING_VALUE.ordinal(),
        A11yMenuShortcut.ShortcutId.ID_NOTIFICATION_VALUE.ordinal(),
        A11yMenuShortcut.ShortcutId.ID_SCREENSHOT_VALUE.ordinal()
    };

    /** Predefined default shortcuts when large button setting is on. */
    private static final int[] LARGE_SHORTCUT_LIST_DEFAULT = {
            A11yMenuShortcut.ShortcutId.ID_ASSISTANT_VALUE.ordinal(),
            A11yMenuShortcut.ShortcutId.ID_A11YSETTING_VALUE.ordinal(),
            A11yMenuShortcut.ShortcutId.ID_POWER_VALUE.ordinal(),
            A11yMenuShortcut.ShortcutId.ID_RECENT_VALUE.ordinal(),
            A11yMenuShortcut.ShortcutId.ID_VOLUME_DOWN_VALUE.ordinal(),
            A11yMenuShortcut.ShortcutId.ID_VOLUME_UP_VALUE.ordinal(),
            A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_DOWN_VALUE.ordinal(),
            A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_UP_VALUE.ordinal(),
            A11yMenuShortcut.ShortcutId.ID_LOCKSCREEN_VALUE.ordinal(),
            A11yMenuShortcut.ShortcutId.ID_QUICKSETTING_VALUE.ordinal(),
            A11yMenuShortcut.ShortcutId.ID_NOTIFICATION_VALUE.ordinal(),
            A11yMenuShortcut.ShortcutId.ID_SCREENSHOT_VALUE.ordinal()
    };

    private final AccessibilityMenuService mService;
    private final DisplayManager mDisplayManager;
    private ViewGroup mLayout;
    private WindowManager.LayoutParams mLayoutParameter;
    private A11yMenuViewPager mA11yMenuViewPager;
    private Handler mHandler;
    private AccessibilityManager mAccessibilityManager;

    public A11yMenuOverlayLayout(AccessibilityMenuService service) {
        mService = service;
        mDisplayManager = mService.getSystemService(DisplayManager.class);
        configureLayout();
        mHandler = new Handler(Looper.getMainLooper());
        mAccessibilityManager = mService.getSystemService(AccessibilityManager.class);
    }

    /** Creates Accessibility menu layout and configure layout parameters. */
    public View configureLayout() {
        return configureLayout(A11yMenuViewPager.DEFAULT_PAGE_INDEX);
    }

    // TODO(b/78292783): Find a better way to inflate layout in the test.
    /**
     * Creates Accessibility menu layout, configure layout parameters and apply index to ViewPager.
     *
     * @param pageIndex the index of the ViewPager to show.
     */
    public View configureLayout(int pageIndex) {

        int lastVisibilityState = View.GONE;
        if (mLayout != null) {
            lastVisibilityState = mLayout.getVisibility();
            clearLayout();
        }

        if (mLayoutParameter == null) {
            initLayoutParams();
        }

        final Display display = mDisplayManager.getDisplay(DEFAULT_DISPLAY);
        final Context uiContext = mService.createWindowContext(
                display, TYPE_ACCESSIBILITY_OVERLAY, /* options= */null);
        uiContext.setTheme(R.style.ServiceTheme);
        final WindowManager windowManager = WindowManagerUtils.getWindowManager(uiContext);
        mLayout = new A11yMenuFrameLayout(uiContext);
        updateLayoutPosition(uiContext);
        inflateLayoutAndSetOnTouchListener(mLayout, uiContext);
        mA11yMenuViewPager = new A11yMenuViewPager(mService);
        mA11yMenuViewPager.configureViewPagerAndFooter(mLayout, createShortcutList(), pageIndex);
        windowManager.addView(mLayout, mLayoutParameter);
        mLayout.setVisibility(lastVisibilityState);
        mA11yMenuViewPager.updateFooterState();

        return mLayout;
    }

    public void clearLayout() {
        if (mLayout != null) {
            WindowManager windowManager = WindowManagerUtils.getWindowManager(mLayout.getContext());
            if (windowManager != null) {
                windowManager.removeView(mLayout);
            }
            mLayout.setOnTouchListener(null);
            mLayout = null;
        }
    }

    /** Updates view layout with new layout parameters only. */
    public void updateViewLayout() {
        if (mLayout == null || mLayoutParameter == null) {
            return;
        }
        updateLayoutPosition(mLayout.getContext());
        WindowManager windowManager = WindowManagerUtils.getWindowManager(mLayout.getContext());
        if (windowManager != null) {
            windowManager.updateViewLayout(mLayout, mLayoutParameter);
        }
    }

    private void initLayoutParams() {
        mLayoutParameter = new WindowManager.LayoutParams();
        mLayoutParameter.type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
        mLayoutParameter.format = PixelFormat.TRANSLUCENT;
        mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
        mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
        mLayoutParameter.setTitle(mService.getString(R.string.accessibility_menu_service_name));
    }

    private void inflateLayoutAndSetOnTouchListener(ViewGroup view, @UiContext Context uiContext) {
        LayoutInflater inflater = LayoutInflater.from(uiContext);
        inflater.inflate(R.layout.paged_menu, view);
        view.setOnTouchListener(mService);
    }

    /**
     * Loads shortcut data from default shortcut ID array.
     *
     * @return A list of default shortcuts
     */
    private List<A11yMenuShortcut> createShortcutList() {
        List<A11yMenuShortcut> shortcutList = new ArrayList<>();

        for (int shortcutId :
                (A11yMenuPreferenceFragment.isLargeButtonsEnabled(mService)
                        ? LARGE_SHORTCUT_LIST_DEFAULT : SHORTCUT_LIST_DEFAULT)) {
            if (!isShortcutRestricted(shortcutId)) {
                shortcutList.add(new A11yMenuShortcut(shortcutId));
            }
        }
        return shortcutList;
    }

    @SuppressLint("MissingPermission")
    private boolean isShortcutRestricted(int shortcutId) {
        final UserManager userManager = mService.getSystemService(UserManager.class);
        if (userManager == null) {
            return false;
        }
        final int userId = mService.getUserId();
        final UserHandle userHandle = UserHandle.of(userId);
        if (shortcutId == A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_DOWN_VALUE.ordinal()
                || shortcutId == A11yMenuShortcut.ShortcutId.ID_BRIGHTNESS_UP_VALUE.ordinal()) {
            if (userManager.hasUserRestriction(DISALLOW_CONFIG_BRIGHTNESS)
                    || userManager.hasBaseUserRestriction(DISALLOW_CONFIG_BRIGHTNESS, userHandle)) {
                return true;
            }
        }
        if (shortcutId == A11yMenuShortcut.ShortcutId.ID_VOLUME_DOWN_VALUE.ordinal()
                || shortcutId == A11yMenuShortcut.ShortcutId.ID_VOLUME_UP_VALUE.ordinal()) {
            if (userManager.hasUserRestriction(DISALLOW_ADJUST_VOLUME)
                    || userManager.hasBaseUserRestriction(DISALLOW_ADJUST_VOLUME, userHandle)) {
                return true;
            }
        }
        return false;
    }

    /** Updates a11y menu layout position by configuring layout params. */
    private void updateLayoutPosition(@UiContext @NonNull Context uiContext) {
        WindowManager windowManager = uiContext.getSystemService(WindowManager.class);
        if (windowManager == null) {
            return;
        }
        final Display display = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY);
        final Configuration configuration = mService.getResources().getConfiguration();
        final int orientation = configuration.orientation;
        if (display != null && orientation == Configuration.ORIENTATION_LANDSCAPE) {
            final boolean ltr = configuration.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR;
            switch (display.getRotation()) {
                case Surface.ROTATION_0:
                case Surface.ROTATION_180:
                    mLayoutParameter.gravity =
                            (ltr ? Gravity.END : Gravity.START) | Gravity.BOTTOM
                                    | Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL;
                    mLayoutParameter.width = WindowManager.LayoutParams.WRAP_CONTENT;
                    mLayoutParameter.height = WindowManager.LayoutParams.MATCH_PARENT;
                    mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
                    mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
                    mLayout.setBackgroundResource(R.drawable.shadow_90deg);
                    break;
                case Surface.ROTATION_90:
                case Surface.ROTATION_270:
                    mLayoutParameter.gravity =
                            (ltr ? Gravity.START : Gravity.END) | Gravity.BOTTOM
                                    | Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL;
                    mLayoutParameter.width = WindowManager.LayoutParams.WRAP_CONTENT;
                    mLayoutParameter.height = WindowManager.LayoutParams.MATCH_PARENT;
                    mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
                    mLayoutParameter.flags |= WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
                    mLayout.setBackgroundResource(R.drawable.shadow_270deg);
                    break;
                default:
                    break;
            }
        } else {
            mLayoutParameter.gravity = Gravity.BOTTOM;
            mLayoutParameter.width = WindowManager.LayoutParams.MATCH_PARENT;
            mLayoutParameter.height = WindowManager.LayoutParams.WRAP_CONTENT;
            mLayout.setBackgroundResource(R.drawable.shadow_0deg);
        }
        // Adjusts the y position of a11y menu layout to make the layout not to overlap bottom
        // navigation bar window.
        updateLayoutByWindowInsetsIfNeeded(windowManager);
        mLayout.setOnApplyWindowInsetsListener(
                (view, insets) -> {
                    if (updateLayoutByWindowInsetsIfNeeded(windowManager)) {
                        windowManager.updateViewLayout(mLayout, mLayoutParameter);
                    }
                    return view.onApplyWindowInsets(insets);
                });
    }

    /**
     * Returns {@code true} if the a11y menu layout params
     * should be updated by {@link WindowManager} immediately due to window insets change.
     * This method adjusts the layout position and size to
     * make a11y menu not to overlap navigation bar window.
     */
    private boolean updateLayoutByWindowInsetsIfNeeded(@NonNull WindowManager windowManager) {
        boolean shouldUpdateLayout = false;
        WindowMetrics windowMetrics = windowManager.getCurrentWindowMetrics();
        Insets windowInsets = windowMetrics.getWindowInsets().getInsetsIgnoringVisibility(
                WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
        int xOffset = max(windowInsets.left, windowInsets.right);
        int yOffset = windowInsets.bottom;
        Rect windowBound = windowMetrics.getBounds();
        if (mLayoutParameter.x != xOffset || mLayoutParameter.y != yOffset) {
            mLayoutParameter.x = xOffset;
            mLayoutParameter.y = yOffset;
            shouldUpdateLayout = true;
        }
        // for gestural navigation mode and the landscape mode,
        // the layout height should be decreased by system bar
        // and display cutout inset to fit the new
        // frame size that doesn't overlap the navigation bar window.
        int orientation = mService.getResources().getConfiguration().orientation;
        if (mLayout.getHeight() != mLayoutParameter.height
                && orientation == Configuration.ORIENTATION_LANDSCAPE) {
            mLayoutParameter.height = windowBound.height() - yOffset;
            shouldUpdateLayout = true;
        }
        return shouldUpdateLayout;
    }

    /**
     * Gets the current page index when device configuration changed. {@link
     * AccessibilityMenuService#onConfigurationChanged(Configuration)}
     *
     * @return the current index of the ViewPager.
     */
    public int getPageIndex() {
        if (mA11yMenuViewPager != null) {
            return mA11yMenuViewPager.mViewPager.getCurrentItem();
        }
        return A11yMenuViewPager.DEFAULT_PAGE_INDEX;
    }

    /**
     * Hides a11y menu layout. And return if layout visibility has been changed.
     *
     * @return {@code true} layout visibility is toggled off; {@code false} is unchanged
     */
    public boolean hideMenu() {
        if (mLayout.getVisibility() == View.VISIBLE) {
            mLayout.setVisibility(View.GONE);
            return true;
        }
        return false;
    }

    /** Toggles a11y menu layout visibility. */
    public void toggleVisibility() {
        if (mLayout.getVisibility() == View.VISIBLE) {
            mLayout.setVisibility(View.GONE);
        } else {
            // Reconfigure the shortcut list in case the set of restricted actions has changed.
            mA11yMenuViewPager.configureViewPagerAndFooter(
                    mLayout, createShortcutList(), getPageIndex());
            updateViewLayout();

            mService.performGlobalAction(GLOBAL_ACTION_DISMISS_NOTIFICATION_SHADE);
            mLayout.setVisibility(View.VISIBLE);
        }
    }

    /** Shows hint text on a minimal Snackbar-like text view. */
    public void showSnackbar(String text) {
        final int animationDurationMs = 300;
        final int timeoutDurationMs = mAccessibilityManager.getRecommendedTimeoutMillis(2000,
                AccessibilityManager.FLAG_CONTENT_TEXT);

        final TextView snackbar = mLayout.findViewById(R.id.snackbar);
        if (snackbar == null) {
            return;
        }
        snackbar.setText(text);
        snackbar.setAccessibilityLiveRegion(ACCESSIBILITY_LIVE_REGION_POLITE);

        // Remove any existing fade-out animation before starting any new animations.
        mHandler.removeCallbacksAndMessages(null);

        if (snackbar.getVisibility() != View.VISIBLE) {
            snackbar.setAlpha(0f);
            snackbar.setVisibility(View.VISIBLE);
            snackbar.animate().alpha(1f).setDuration(animationDurationMs).setListener(null);
        }
        mHandler.postDelayed(() -> snackbar.animate().alpha(0f).setDuration(
                animationDurationMs).setListener(
                new AnimatorListenerAdapter() {
                        @Override
                        public void onAnimationEnd(@NonNull Animator animation) {
                            snackbar.setVisibility(View.GONE);
                        }
                    }), timeoutDurationMs);
    }

    private class A11yMenuFrameLayout extends FrameLayout {
        A11yMenuFrameLayout(@UiContext @NonNull Context context) {
            super(context);
        }

        @Override
        public void dispatchConfigurationChanged(Configuration newConfig) {
            super.dispatchConfigurationChanged(newConfig);
            mA11yMenuViewPager.mA11yMenuFooter.updateRightToLeftDirection(newConfig);
        }
    }
}

在 Android 系统中,A11yMenuOverlayLayout.java辅助功能菜单(Accessibility Menu) 核心 UI 逻辑的实现类。它负责定义和管理那个显示在屏幕上的大图标菜单,方便有特殊需求的用户控制设备(如调节音量、亮度、截屏等)。

以下是对该类的详细技术解析:

1. 核心定位

A11yMenuOverlayLayout 是一个自定义布局类(通常继承自 FrameLayout),它作为辅助功能菜单的根视图容器。它的主要职责是:

  • 承载 UI 组件:管理菜单中的图标、翻页指示器(ViewPager/Dots)以及背景遮罩。
  • 管理布局状态:根据屏幕方向(横屏/竖屏)或用户设置(大按钮模式/普通模式)动态调整布局。
  • 处理窗口属性:通过 WindowManager.LayoutParams 将菜单作为系统级浮窗(Overlay)显示。

2. UI 架构结构

该菜单的 UI 层次结构通常如下:

  • A11yMenuOverlayLayout (Root): 顶层容器,处理外部点击关闭菜单的逻辑。
  • ViewPager: 因为快捷键很多,菜单通常分为多页,使用 ViewPager 实现左右滑动。
  • GridView / RecyclerView: 每一页内部是一个网格布局,排列具体的快捷按钮。
  • Shortcut Icons: 单个功能按钮(如“电源”、“音量”)。

3. 关键成员变量与常量

在源码中,你会看到以下关键部分:

  • SHORTCUT_LIST_DEFAULT: 定义了默认模式下快捷键的顺序和组合。
  • LARGE_SHORTCUT_LIST_DEFAULT: 定义了“大按钮模式”下的快捷键组合(通常每页显示的按钮更少,但尺寸更大)。
  • mLayoutParameter: 类型为 WindowManager.LayoutParams。这决定了菜单是一个系统悬浮窗,具有 TYPE_ACCESSIBILITY_OVERLAY 属性,能够显示在其他应用之上。

4. 核心方法解析

configureLayout() / updateLayout()

这些方法负责计算菜单的大小和位置。

  • 它会根据当前屏幕的 DisplayMetrics 来计算菜单应该占据的宽度和高度。
  • 它会监听配置变化(如旋转屏幕),并调用这些方法重新对齐 UI,确保菜单始终处于用户易于触及的位置。
show()hide()
  • show(): 当用户触发辅助功能快捷方式(如双指上滑或点击悬浮按钮)时,AccessibilityMenuService 会调用此方法。它会将视图添加到 WindowManager 中,并触发显示动画。
  • hide(): 移除视图或播放收起动画。
监听逻辑

该类通常会实现一个处理点击事件的监听器。当用户点击某个图标(如“截屏”)时:

  1. Layout 捕获点击。
  2. 将对应的 ShortcutId 发回给 AccessibilityMenuService
  3. Service 调用 AccessibilityService#performGlobalAction() 来执行真正的系统操作。

5. 与 Service 的协作流程

A11yMenuOverlayLayout 并不直接执行“截屏”或“调音量”的操作,它只负责表现层

  1. 用户操作:点击菜单中的“音量上”。
  2. Layout 层A11yMenuOverlayLayout 检测到点击,识别出 ID 为 ID_VOLUME_UP
  3. 回调 Service:通过接口回调给 AccessibilityMenuService
  4. 系统执行:Service 调用 Android 系统 API(如 AudioManager)来改变音量。

6. 为什么它在 SystemUI 中很重要?

  • 性能:作为系统级 Overlay,它必须极其轻量,不能阻塞主线程。
  • 兼容性:它需要适配各种屏幕形状(刘海屏、折叠屏)和分辨率。
  • 无障碍性:它自身也必须是可访问的。这意味着它内部的每个按钮都必须有正确的 contentDescription,以便 TalkBack 等屏幕阅读器能正确读出功能。

总结

如果你在阅读这个文件的源码,你应该重点关注它如何使用 WindowManager 动态添加视图,以及它是如何通过 ViewPager + Adapter 的模式来解耦不同功能的图标生成的。它是 Android 保持界面简洁的同时提供强大辅助功能的关键工程实现。

6. A11yMenuViewPager.java

目录:frameworks/base/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuViewPager.java
链接:
https://cs.android.com/android/platform/superproject/+/android-latest-release:frameworks/base/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuViewPager.java?hl=zh-cn

/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.accessibility.accessibilitymenu.view;

import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Insets;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
import android.widget.GridView;

import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;

import com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService;
import com.android.systemui.accessibility.accessibilitymenu.R;
import com.android.systemui.accessibility.accessibilitymenu.activity.A11yMenuSettingsActivity.A11yMenuPreferenceFragment;
import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut;
import com.android.systemui.accessibility.accessibilitymenu.view.A11yMenuFooter.A11yMenuFooterCallBack;
import com.android.systemui.utils.windowmanager.WindowManagerUtils;

import java.util.ArrayList;
import java.util.List;

/**
 * This class handles UI for viewPager and footer.
 * It displays grid pages containing all shortcuts in viewPager,
 * and handles the click events from footer to switch between pages.
 */
public class A11yMenuViewPager {

    /** The default index of the ViewPager. */
    public static final int DEFAULT_PAGE_INDEX = 0;

    /**
     * The class holds the static parameters for grid view when large button settings is on/off.
     */
    public static final class GridViewParams {
        /** Total shortcuts count in the grid view when large button settings is off. */
        public static final int GRID_ITEM_COUNT = 9;

        /** The number of columns in the grid view when large button settings is off. */
        public static final int GRID_COLUMN_COUNT = 3;

        /** Total shortcuts count in the grid view when large button settings is on. */
        public static final int LARGE_GRID_ITEM_COUNT = 4;

        /** The number of columns in the grid view when large button settings is on. */
        public static final int LARGE_GRID_COLUMN_COUNT = 2;

        /**
         * Returns the number of items in the grid view.
         *
         * @param context The parent context
         * @return Grid item count
         */
        public static int getGridItemCount(Context context) {
            return A11yMenuPreferenceFragment.isLargeButtonsEnabled(context)
                   ? LARGE_GRID_ITEM_COUNT
                   : GRID_ITEM_COUNT;
        }

        /**
         * Returns the number of columns in the grid view.
         *
         * @param context The parent context
         * @return Grid column count
         */
        public static int getGridColumnCount(Context context) {
            return A11yMenuPreferenceFragment.isLargeButtonsEnabled(context)
                   ? LARGE_GRID_COLUMN_COUNT
                   : GRID_COLUMN_COUNT;
        }

        /**
         * Returns the number of rows in the grid view.
         *
         * @param context The parent context
         * @return Grid row count
         */
        public static int getGridRowCount(Context context) {
            return A11yMenuPreferenceFragment.isLargeButtonsEnabled(context)
                   ? (LARGE_GRID_ITEM_COUNT / LARGE_GRID_COLUMN_COUNT)
                   : (GRID_ITEM_COUNT / GRID_COLUMN_COUNT);
        }

        /**
         * Separates a provided list of accessibility shortcuts into multiple sub-lists.
         * Does not modify the original list.
         *
         * @param pageItemCount The maximum size of an individual sub-list.
         * @param shortcutList The list of shortcuts to be separated into sub-lists.
         * @return A list of shortcut sub-lists.
         */
        public static List<List<A11yMenuShortcut>> generateShortcutSubLists(
                int pageItemCount, List<A11yMenuShortcut> shortcutList) {
            int start = 0;
            int end;
            int shortcutListSize = shortcutList.size();
            List<List<A11yMenuShortcut>> subLists = new ArrayList<>();
            while (start < shortcutListSize) {
                end = Math.min(start + pageItemCount, shortcutListSize);
                subLists.add(shortcutList.subList(start, end));
                start = end;
            }
            return subLists;
        }

        private GridViewParams() {}
    }

    private final AccessibilityMenuService mService;

    /**
     * The pager widget, which handles animation and allows swiping horizontally to access previous
     * and next gridView pages.
     */
    protected ViewPager2 mViewPager;

    private ViewPagerAdapter mViewPagerAdapter;
    private final List<GridView> mGridPageList = new ArrayList<>();

    /** The footer, which provides buttons to switch between pages */
    protected A11yMenuFooter mA11yMenuFooter;

    /** The shortcut list intended to show in grid pages of viewPager */
    private List<A11yMenuShortcut> mA11yMenuShortcutList;

    /** The container layout for a11y menu. */
    private ViewGroup mA11yMenuLayout;

    public A11yMenuViewPager(AccessibilityMenuService service) {
        this.mService = service;
    }

    /**
     * Configures UI for view pager and footer.
     *
     * @param a11yMenuLayout the container layout for a11y menu
     * @param shortcutDataList the data list need to show in view pager
     * @param pageIndex the index of ViewPager to show
     */
    public void configureViewPagerAndFooter(
            ViewGroup a11yMenuLayout, List<A11yMenuShortcut> shortcutDataList, int pageIndex) {
        this.mA11yMenuLayout = a11yMenuLayout;
        mA11yMenuShortcutList = shortcutDataList;
        initViewPager();
        initChildPage();
        if (mA11yMenuFooter == null) {
            mA11yMenuFooter = new A11yMenuFooter(a11yMenuLayout, mFooterCallbacks);
        }
        mA11yMenuFooter.updateRightToLeftDirection(
                a11yMenuLayout.getResources().getConfiguration());
        updateFooterState();
        registerOnGlobalLayoutListener();
        goToPage(pageIndex, /*shouldAnimate=*/ false);
    }

    /** Initializes viewPager and its adapter. */
    private void initViewPager() {
        mViewPager = mA11yMenuLayout.findViewById(R.id.view_pager);
        mViewPagerAdapter = new ViewPagerAdapter(mService);
        mViewPager.setOffscreenPageLimit(2);
        mViewPager.setAdapter(mViewPagerAdapter);
        mViewPager.setOverScrollMode(View.OVER_SCROLL_NEVER);
        mViewPager.registerOnPageChangeCallback(
                new ViewPager2.OnPageChangeCallback() {
                    @Override
                    public void onPageSelected(int position) {
                        updateFooterState();
                    }
                });
    }

    /** Creates child pages of viewPager by the length of shortcuts and initializes them. */
    private void initChildPage() {
        if (mA11yMenuShortcutList == null || mA11yMenuShortcutList.isEmpty()) {
            return;
        }

        if (!mGridPageList.isEmpty()) {
            mGridPageList.clear();
        }

        mViewPagerAdapter.set(GridViewParams.generateShortcutSubLists(
                GridViewParams.getGridItemCount(mService), mA11yMenuShortcutList));
    }

    /** Updates footer's state by index of current page in view pager. */
    public void updateFooterState() {
        int currentPage = mViewPager.getCurrentItem();
        int lastPage = mViewPager.getAdapter().getItemCount() - 1;
        mA11yMenuFooter.getPreviousPageBtn().setEnabled(currentPage > 0);
        mA11yMenuFooter.getNextPageBtn().setEnabled(currentPage < lastPage);
    }

    private void goToPage(int pageIndex, boolean shouldAnimate) {
        if (mViewPager == null) {
            return;
        }
        if ((pageIndex >= 0) && (pageIndex < mViewPager.getAdapter().getItemCount())) {
            mViewPager.setCurrentItem(pageIndex, shouldAnimate);
        }
    }

    /** Registers OnGlobalLayoutListener to adjust menu UI by running callback at first time. */
    private void registerOnGlobalLayoutListener() {
        mA11yMenuLayout
                .getViewTreeObserver()
                .addOnGlobalLayoutListener(
                        new OnGlobalLayoutListener() {

                            boolean mIsFirstTime = true;

                            @Override
                            public void onGlobalLayout() {
                                if (!mIsFirstTime) {
                                    return;
                                }

                                if (mViewPagerAdapter.getItemCount() == 0) {
                                    return;
                                }

                                RecyclerView.ViewHolder viewHolder =
                                        ((RecyclerView) mViewPager.getChildAt(0))
                                                .findViewHolderForAdapterPosition(0);
                                if (viewHolder == null) {
                                    return;
                                }
                                GridView firstGridView = (GridView) viewHolder.itemView;
                                if (firstGridView == null
                                        || firstGridView.getChildAt(0) == null) {
                                    return;
                                }

                                mIsFirstTime = false;

                                int gridItemHeight = firstGridView.getChildAt(0)
                                                .getMeasuredHeight();
                                adjustMenuUISize(gridItemHeight);
                            }
                        });
    }

    /**
     * Adjusts menu UI to fit both landscape and portrait mode.
     *
     * <ol>
     *   <li>Adjust view pager's height.
     *   <li>Adjust vertical interval between grid items.
     *   <li>Adjust padding in view pager.
     * </ol>
     */
    private void adjustMenuUISize(int gridItemHeight) {
        final int rowsInGridView = GridViewParams.getGridRowCount(mService);
        final int defaultMargin =
                (int) mService.getResources().getDimension(R.dimen.a11ymenu_layout_margin);
        final int topMargin = (int) mService.getResources().getDimension(R.dimen.table_margin_top);
        final int displayMode = mService.getResources().getConfiguration().orientation;
        int viewPagerHeight = mViewPager.getMeasuredHeight();

        if (displayMode == Configuration.ORIENTATION_PORTRAIT) {
            // In portrait mode, we only need to adjust view pager's height to match its
            // child's height.
            viewPagerHeight = gridItemHeight * rowsInGridView + defaultMargin + topMargin;
        } else if (displayMode == Configuration.ORIENTATION_LANDSCAPE) {
            // In landscape mode, we need to adjust view pager's height to match screen height
            // and adjust its child too,
            // because a11y menu layout height is limited by the screen height.
            DisplayMetrics displayMetrics = mService.getResources().getDisplayMetrics();
            float densityScale = (float) displayMetrics.densityDpi
                    / DisplayMetrics.DENSITY_DEVICE_STABLE;
            // Keeps footer window height unchanged no matter the density is changed.
            mA11yMenuFooter.adjustFooterToDensityScale(densityScale);
            // Adjust the view pager height for system bar and display cutout insets.
            WindowManager windowManager = WindowManagerUtils
                    .getWindowManager(mA11yMenuLayout.getContext());
            WindowMetrics windowMetric = windowManager.getCurrentWindowMetrics();
            Insets windowInsets = windowMetric.getWindowInsets().getInsetsIgnoringVisibility(
                    WindowInsets.Type.systemBars() | WindowInsets.Type.displayCutout());
            viewPagerHeight =
                    windowMetric.getBounds().height()
                            - mA11yMenuFooter.getHeight()
                            - windowInsets.bottom;
            // Sets vertical interval between grid items.
            int interval =
                    (viewPagerHeight - topMargin - defaultMargin
                            - (rowsInGridView * gridItemHeight))
                            / (rowsInGridView + 1);
            // The interval is negative number when the viewPagerHeight is not able to fit
            // the grid items, which result in text overlapping.
            // Adjust the interval to 0 could solve the issue.
            interval = Math.max(interval, 0);
            mViewPagerAdapter.setVerticalSpacing(interval);

            // Sets padding to view pager.
            final int finalMarginTop = interval + topMargin;
            mViewPager.setPadding(0, finalMarginTop, 0, defaultMargin);
        }
        final ViewGroup.LayoutParams layoutParams = mViewPager.getLayoutParams();
        layoutParams.height = viewPagerHeight;
        mViewPager.setLayoutParams(layoutParams);
    }

    /** Callback object to handle click events from A11yMenuFooter */
    protected A11yMenuFooterCallBack mFooterCallbacks =
            new A11yMenuFooterCallBack() {
                @Override
                public void onPreviousButtonClicked() {
                    // Moves to previous page.
                    int targetPage = mViewPager.getCurrentItem() - 1;
                    goToPage(targetPage, /*shouldAnimate=*/ true);
                    updateFooterState();
                }

                @Override
                public void onNextButtonClicked() {
                    // Moves to next page.
                    int targetPage = mViewPager.getCurrentItem() + 1;
                    goToPage(targetPage, /*shouldAnimate=*/ true);
                    updateFooterState();
                }
            };
}

A11yMenuViewPager.java 是 Android 系统中“无障碍功能菜单”(Accessibility Menu)的核心 UI 组件之一。该菜单是 Android 为不便操作物理按键或精细触摸屏的用户提供的一个大图标悬浮菜单(包含截图、音量调节、锁定屏幕等功能)。

这个类继承自标准的 androidx.viewpager.widget.ViewPager,并针对无障碍场景进行了定制。以下是该文件的详细解析:

1. 核心定位与作用

在 Android 的无障碍菜单中,由于功能图标较多,无法在一个屏幕内全部展示。A11yMenuViewPager 的主要作用是:

  • 承载分页内容:它将菜单功能图标(如“电源”、“音量”、“通知”等)分布在不同的页面上。
  • 适配布局模式:根据系统设置,它支持 3x3 栅格(默认)或 2x2 栅格(大按钮模式)的页面切换。
  • 优化交互体验:确保在使用 TalkBack(屏幕阅读器)或其他辅助工具时,页面的切换和焦点跳转符合无障碍规范。

2. 代码关键特性解析

A. 页面测量与尺寸控制 (onMeasure)

A11yMenuViewPager 通常会重写 onMeasure 方法。

  • 原因:标准的 ViewPager 在测量高度时有时会表现得比较“贪婪”或不准确。由于无障碍菜单是一个悬浮窗,需要根据图标的大小(普通 vs 大按钮)动态计算并锁定高度,以防止菜单在切换页面时出现抖动或遮挡导航栏。
B. 焦点管理 (Focus Management)

这是无障碍组件最关键的部分。该类通常会处理以下逻辑:

  • 焦点环绕:当用户在最后一页的最后一个图标向后导航时,或者在第一页向前导航时,如何处理焦点。
  • 自动化滚动:当 TalkBack 用户的焦点移动到当前页面不可见的项时,A11yMenuViewPager 会配合 PagerAdapter 自动滑动到下一页。
C. 数据绑定与适配器

它通常与 A11yMenuAdapter 配合使用。

  • 它不直接控制图标,而是作为一个容器。
  • 适配器会根据 ShortcutId 列表,将“截图”、“电源”等功能分配到不同的 GridViewRecyclerView 中,然后再放入这个 ViewPager。

3. 与其他组件的关系

为了理解这个文件,需要看它在 com.android.systemui.accessibility.accessibilitymenu 包下的位置:

  1. AccessibilityMenuService: 整个功能的后端服务,负责监听手势和分发指令。
  2. A11yMenuOverlayLayout: 整个菜单的最外层布局容器。
  3. A11yMenuViewPager (即本文件): 位于布局中间,负责左右滑动切换图标页。
  4. A11yMenuAdapter: 为 ViewPager 提供具体页面内容的适配器。

4. 为什么不直接用原生的 ViewPager?

虽然它继承自 ViewPager,但针对系统级无障碍菜单做了特殊加固:

  • 稳定性:作为 SystemUI 的一部分,它必须极其稳定,不能因为页面切换导致系统界面(Surface)崩溃。
  • RTL 支持:在阿拉伯语等从右向左读的语言环境下,确保滑动方向和图标排列顺序完全正确。
  • 窗口类型适配:它运行在 TYPE_ACCESSIBILITY_OVERLAY 类型的窗口中,这与普通应用的窗口处理逻辑略有不同。

总结

A11yMenuViewPager.java 是 Android 实现易用性的重要一环。它不仅是一个简单的滑动控件,更是连接“复杂功能集合”与“简单物理操作”的桥梁。通过分页逻辑,它让手指不便的用户能够通过大面积的点击和简单的滑动来完全控制手机。

7. ViewPagerAdapter.java

目录:
frameworks/base/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/ViewPagerAdapter.java
链接:
https://cs.android.com/android/platform/superproject/+/android-latest-release:frameworks/base/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/ViewPagerAdapter.java?hl=zh-cn

/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.accessibility.accessibilitymenu.view;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.GridView;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService;
import com.android.systemui.accessibility.accessibilitymenu.R;
import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut;

import java.util.List;

/** The pager adapter, which provides the pages to the view pager widget. */
class ViewPagerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    /** List of shortcuts, split into sub lists per page */
    private List<List<A11yMenuShortcut>> mShortcutList;
    private final AccessibilityMenuService mService;
    private int mVerticalSpacing = 0;

    ViewPagerAdapter(AccessibilityMenuService service) {
        mService = service;
    }

    public void setVerticalSpacing(int spacing) {
        if (mVerticalSpacing != spacing) {
            mVerticalSpacing = spacing;
            notifyDataSetChanged();
        }
    }

    public void set(List<List<A11yMenuShortcut>> tList) {
        mShortcutList = tList;
        notifyDataSetChanged();
    }

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
        View view = inflater.inflate(R.layout.grid_view, parent, false);
        return new MenuViewHolder(view.findViewById(R.id.gridview));
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        A11yMenuAdapter adapter = new A11yMenuAdapter(
                mService, mShortcutList.get(position));
        GridView gridView = (GridView) holder.itemView;
        gridView.setNumColumns(A11yMenuViewPager.GridViewParams.getGridColumnCount(mService));
        gridView.setAdapter(adapter);
        gridView.setVerticalSpacing(mVerticalSpacing);
    }

    @Override
    public int getItemCount() {
        if (mShortcutList == null) {
            return 0;
        }
        return mShortcutList.size();
    }

    static class MenuViewHolder extends RecyclerView.ViewHolder {
        MenuViewHolder(View itemView) {
            super(itemView);
        }
    }
}

这个 ViewPagerAdapter.java 文件是 Android 系统源码中 SystemUI 模块的一部分,专门用于“辅助功能菜单”(Accessibility Menu)的界面呈现。

简单来说,它的作用是管理辅助功能菜单中多页图标的切换逻辑。当你在手机上开启“辅助功能菜单”并看到一排排大图标(如:截屏、音量、亮度等)时,如果图标太多分成了几页,这个类就负责将这些图标组织成滑动的页面。

以下是该代码的核心功能和实现逻辑的详细分析:

1. 类定义与背景

  • 路径com.android.systemui.accessibility.accessibilitymenu.view.ViewPagerAdapter
  • 继承关系:继承自 androidx.viewpager.widget.PagerAdapter
  • 用途:它是经典 ViewPager 的适配器,用于在 AccessibilityMenu 的弹窗中展示图标网格(GridView)。

2. 核心成员变量

该类通常持有以下关键数据:

  • Context: 用于加载布局和资源。
  • List<View>List<GridView>: 这是一个核心列表,存储了每一页的视图对象。每一页通常是一个 GridView,里面排列着辅助功能的各种按钮(如“电源”、“音量”等)。

3. 核心方法解析

instantiateItem(ViewGroup container, int position)

这是 PagerAdapter 最重要的方法。

  • 逻辑:根据当前的页码(position),从预先创建好的视图列表中获取对应的 GridView 页面。
  • 动作:将该页面添加到 ViewPager(即 container)中。
  • 返回值:返回该页面的 View 对象。
destroyItem(ViewGroup container, int position, Object object)
  • 逻辑:当用户滑动到远处,某些页面不再需要显示时,从 ViewPager 中移除该页面以节省内存。
getCount()
  • 逻辑:返回总页数。这通常取决于当前的配置(如:是否开启了“大按钮”模式,大按钮模式下每页显示的图标更少,因此总页数会增加)。
isViewFromObject(View view, Object object)
  • 逻辑:标准的 PagerAdapter 实现,通常直接返回 view == object,用于判断视图与 Key 对象是否关联。

4. 业务逻辑细节:图标的分页

在辅助功能菜单中,图标的排列不是随意的。ViewPagerAdapter 配合其所在的 AccessibilityMenuView 执行以下逻辑:

  1. 计算页数:根据屏幕大小和设置(普通 vs 大图标),计算每一页能放几个图标。
  2. 创建 GridView:为每一页创建一个网格布局。
  3. 填充数据:将特定的辅助功能动作(例如 ACTION_SCREENSHOT)分配到对应的网格单元格中。

5. 与系统的交互

这个类不仅是简单的 UI 代码,它还涉及到:

  • 响应配置变化:当用户在设置中开启“大按钮”时,适配器会重新计算并调用 notifyDataSetChanged() 来刷新页面。
  • 辅助功能服务连接:它展示的任务(如调整音量、打开通知栏)最终会通过 AccessibilityService 发送指令给系统内核。

总结

ViewPagerAdapter.java 在这里扮演的是“页面搬运工”的角色。它确保了辅助功能菜单能够流畅地以翻页形式展示大量的控制按钮,使得手部操作不便的用户能够通过简单的滑动和点击,控制手机的高级功能。

如果你在修改这个文件,通常是为了:

  1. 自定义布局:改变菜单每一页的显示样式。
  2. 调整分页算法:改变每一页显示的图标数量。
  3. 适配新设备:针对折叠屏或特殊分辨率调整菜单的适配逻辑。

8. AccessibilityMenuService.java

目录:
frameworks/base/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java
链接:
https://cs.android.com/android/platform/superproject/+/android-latest-release:frameworks/base/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/AccessibilityMenuService.java?hl=zh-cn

/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.accessibility.accessibilitymenu;

import android.Manifest;
import android.accessibilityservice.AccessibilityButtonController;
import android.accessibilityservice.AccessibilityService;
import android.app.KeyguardManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Configuration;
import android.hardware.display.BrightnessInfo;
import android.hardware.display.DisplayManager;
import android.media.AudioManager;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.provider.Settings;
import android.util.Log;
import android.view.Display;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;

import androidx.preference.PreferenceManager;

import com.android.settingslib.display.BrightnessUtils;
import com.android.systemui.accessibility.accessibilitymenu.model.A11yMenuShortcut.ShortcutId;
import com.android.systemui.accessibility.accessibilitymenu.view.A11yMenuOverlayLayout;

import java.util.List;

/** @hide */
public class AccessibilityMenuService extends AccessibilityService
        implements View.OnTouchListener {

    public static final String PACKAGE_NAME = AccessibilityMenuService.class.getPackageName();
    public static final String PACKAGE_TESTS = ".tests";
    public static final String INTENT_TOGGLE_MENU = ".toggle_menu";
    public static final String INTENT_HIDE_MENU = ".hide_menu";
    public static final String INTENT_GLOBAL_ACTION = ".global_action";
    public static final String INTENT_GLOBAL_ACTION_EXTRA = "GLOBAL_ACTION";
    public static final String INTENT_OPEN_BLOCKED = "OPEN_BLOCKED";

    private static final String TAG = "A11yMenuService";
    private static final long BUFFER_MILLISECONDS_TO_PREVENT_UPDATE_FAILURE = 100L;
    private static final long HIDE_UI_DELAY_MS = 100L;

    private static final int BRIGHTNESS_UP_INCREMENT_GAMMA =
            (int) Math.ceil(BrightnessUtils.GAMMA_SPACE_MAX * 0.11f);
    private static final int BRIGHTNESS_DOWN_INCREMENT_GAMMA =
            (int) -Math.ceil(BrightnessUtils.GAMMA_SPACE_MAX * 0.11f);

    private long mLastTimeTouchedOutside = 0L;
    // Timeout used to ignore the A11y button onClick() when ACTION_OUTSIDE is also received on
    // clicking on the A11y button.
    public static final long BUTTON_CLICK_TIMEOUT = 200;

    private A11yMenuOverlayLayout mA11yMenuLayout;
    private SharedPreferences mPrefs;

    private static boolean sInitialized = false;

    private AudioManager mAudioManager;

    // TODO(b/136716947): Support multi-display once a11y framework side is ready.
    private DisplayManager mDisplayManager;

    private KeyguardManager mKeyguardManager;

    private final DisplayManager.DisplayListener mDisplayListener =
            new DisplayManager.DisplayListener() {
                int mRotation;

                @Override
                public void onDisplayAdded(int displayId) {
                }

                @Override
                public void onDisplayRemoved(int displayId) {
                    // TODO(b/136716947): Need to reset A11yMenuOverlayLayout by display id.
                }

                @Override
                public void onDisplayChanged(int displayId) {
                    if (mA11yMenuLayout == null) {
                        return;
                    }
                    Display display = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY);
                    if (mRotation != display.getRotation()) {
                        mRotation = display.getRotation();
                        mA11yMenuLayout.updateViewLayout();
                    }
                }
            };

    private final BroadcastReceiver mHideMenuReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            mA11yMenuLayout.hideMenu();
        }
    };

    private final BroadcastReceiver mToggleMenuReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            toggleVisibility();
        }
    };

    /**
     * Update a11y menu layout when large button setting is changed.
     */
    private final OnSharedPreferenceChangeListener mSharedPreferenceChangeListener =
            (SharedPreferences prefs, String key) -> {
                {
                    if (key.equals(getString(R.string.pref_large_buttons))) {
                        mA11yMenuLayout.configureLayout();
                    }
                }
            };

    // Update layout.
    private final Handler mHandler = new Handler(Looper.getMainLooper());
    private final Runnable mOnConfigChangedRunnable = new Runnable() {
        @Override
        public void run() {
            if (!sInitialized) {
                return;
            }
            // Re-assign theme to service after onConfigurationChanged
            getTheme().applyStyle(R.style.ServiceTheme, true);
            // Caches & updates the page index to ViewPager when a11y menu is refreshed.
            // Otherwise, the menu page would reset on a UI update.
            int cachedPageIndex = mA11yMenuLayout.getPageIndex();
            mA11yMenuLayout.configureLayout(cachedPageIndex);
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        setTheme(R.style.ServiceTheme);

        getAccessibilityButtonController().registerAccessibilityButtonCallback(
                new AccessibilityButtonController.AccessibilityButtonCallback() {
                    /**
                     * {@inheritDoc}
                     */
                    @Override
                    public void onClicked(AccessibilityButtonController controller) {
                        toggleVisibility();
                    }

                    /**
                     * {@inheritDoc}
                     */
                    @Override
                    public void onAvailabilityChanged(AccessibilityButtonController controller,
                            boolean available) {}
                }
        );
    }

    @Override
    public void onDestroy() {
        if (mHandler.hasCallbacks(mOnConfigChangedRunnable)) {
            mHandler.removeCallbacks(mOnConfigChangedRunnable);
        }

        super.onDestroy();
    }

    @Override
    protected void onServiceConnected() {
        mA11yMenuLayout = new A11yMenuOverlayLayout(this);

        IntentFilter hideMenuFilter = new IntentFilter();
        hideMenuFilter.addAction(Intent.ACTION_SCREEN_OFF);
        hideMenuFilter.addAction(INTENT_HIDE_MENU);

        // Including WRITE_SECURE_SETTINGS enforces that we only listen to apps
        // with the restricted WRITE_SECURE_SETTINGS permission who broadcast this intent.
        registerReceiver(mHideMenuReceiver, hideMenuFilter,
                Manifest.permission.WRITE_SECURE_SETTINGS, null,
                Context.RECEIVER_EXPORTED);
        registerReceiver(mToggleMenuReceiver,
                new IntentFilter(INTENT_TOGGLE_MENU),
                Manifest.permission.WRITE_SECURE_SETTINGS, null,
                Context.RECEIVER_EXPORTED);

        mPrefs = PreferenceManager.getDefaultSharedPreferences(this);
        mPrefs.registerOnSharedPreferenceChangeListener(mSharedPreferenceChangeListener);


        mDisplayManager = getSystemService(DisplayManager.class);
        mDisplayManager.registerDisplayListener(mDisplayListener, null);
        mAudioManager = getSystemService(AudioManager.class);
        mKeyguardManager = getSystemService(KeyguardManager.class);

        sInitialized = true;
    }

    @Override
    public void onAccessibilityEvent(AccessibilityEvent event) {}

    /**
     * This method would notify service when device configuration, such as display size,
     * localization, orientation or theme, is changed.
     *
     * @param newConfig the new device configuration.
     */
    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        // Prevent update layout failure
        // if multiple onConfigurationChanged are called at the same time.
        if (mHandler.hasCallbacks(mOnConfigChangedRunnable)) {
            mHandler.removeCallbacks(mOnConfigChangedRunnable);
        }
        mHandler.postDelayed(
                mOnConfigChangedRunnable, BUFFER_MILLISECONDS_TO_PREVENT_UPDATE_FAILURE);
    }

    /**
     * Performs global action and broadcasts an intent indicating the action was performed.
     * This is unnecessary for any current functionality, but is used for testing.
     * Refer to {@code performGlobalAction()}.
     *
     * @param globalAction Global action to be performed.
     * @return {@code true} if successful, {@code false} otherwise.
     */
    private boolean performGlobalActionInternal(int globalAction) {
        Intent intent = new Intent(INTENT_GLOBAL_ACTION);
        intent.putExtra(INTENT_GLOBAL_ACTION_EXTRA, globalAction);
        intent.setPackage(PACKAGE_NAME + PACKAGE_TESTS);
        sendBroadcast(intent);
        Log.i("A11yMenuService", "Broadcasting global action " + globalAction);
        return performGlobalAction(globalAction);
    }

    /**
     * Handles click events of shortcuts.
     *
     * @param view the shortcut button being clicked.
     */
    public void handleClick(View view) {
        // Shortcuts are repeatable in a11y menu rather than unique, so use tag ID to handle.
        int viewTag = (int) view.getTag();

        // First check if this was a shortcut which should keep a11y menu visible. If so,
        // perform the shortcut and return without hiding the UI.
        if (viewTag == ShortcutId.ID_BRIGHTNESS_UP_VALUE.ordinal()) {
            adjustBrightness(BRIGHTNESS_UP_INCREMENT_GAMMA);
            return;
        } else if (viewTag == ShortcutId.ID_BRIGHTNESS_DOWN_VALUE.ordinal()) {
            adjustBrightness(BRIGHTNESS_DOWN_INCREMENT_GAMMA);
            return;
        } else if (viewTag == ShortcutId.ID_VOLUME_UP_VALUE.ordinal()) {
            adjustVolume(AudioManager.ADJUST_RAISE);
            return;
        } else if (viewTag == ShortcutId.ID_VOLUME_DOWN_VALUE.ordinal()) {
            adjustVolume(AudioManager.ADJUST_LOWER);
            return;
        }

        // Hide the a11y menu UI before performing the following shortcut actions.
        mA11yMenuLayout.hideMenu();

        if (viewTag == ShortcutId.ID_ASSISTANT_VALUE.ordinal()) {
            // Always restart the voice command activity, so that the UI is reloaded.
            startActivityIfIntentIsSafe(
                    new Intent(Intent.ACTION_VOICE_COMMAND),
                    Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
        } else if (viewTag == ShortcutId.ID_A11YSETTING_VALUE.ordinal()) {
            startActivityIfIntentIsSafe(
                    new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS),
                    Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        } else if (viewTag == ShortcutId.ID_POWER_VALUE.ordinal()) {
            performGlobalActionInternal(GLOBAL_ACTION_POWER_DIALOG);
        } else if (viewTag == ShortcutId.ID_RECENT_VALUE.ordinal()) {
            performGlobalActionInternal(GLOBAL_ACTION_RECENTS);
        } else if (viewTag == ShortcutId.ID_LOCKSCREEN_VALUE.ordinal()) {
            // Delay before locking the screen to give time for the UI to close.
            mHandler.postDelayed(
                    () -> performGlobalActionInternal(GLOBAL_ACTION_LOCK_SCREEN),
                    HIDE_UI_DELAY_MS);
        } else if (viewTag == ShortcutId.ID_QUICKSETTING_VALUE.ordinal()) {
            performGlobalActionInternal(GLOBAL_ACTION_QUICK_SETTINGS);
        } else if (viewTag == ShortcutId.ID_NOTIFICATION_VALUE.ordinal()) {
            performGlobalActionInternal(GLOBAL_ACTION_NOTIFICATIONS);
        } else if (viewTag == ShortcutId.ID_SCREENSHOT_VALUE.ordinal()) {
            mHandler.postDelayed(
                    () -> performGlobalActionInternal(GLOBAL_ACTION_TAKE_SCREENSHOT),
                    HIDE_UI_DELAY_MS);
        }
    }

    /**
     * Adjusts brightness using the same logic and utils class as the SystemUI brightness slider.
     *
     * @see BrightnessUtils
     * @see com.android.systemui.settings.brightness.BrightnessController
     * @param increment The increment amount in gamma-space
     */
    private void adjustBrightness(int increment) {
        Display display = mDisplayManager.getDisplay(Display.DEFAULT_DISPLAY);
        BrightnessInfo info = display.getBrightnessInfo();
        int gamma = BrightnessUtils.convertLinearToGammaFloat(
                info.brightness,
                info.brightnessMinimum,
                info.brightnessMaximum
        );
        gamma = Math.max(
                BrightnessUtils.GAMMA_SPACE_MIN,
                Math.min(BrightnessUtils.GAMMA_SPACE_MAX, gamma + increment));

        float brightness = BrightnessUtils.convertGammaToLinearFloat(
                gamma,
                info.brightnessMinimum,
                info.brightnessMaximum
        );
        mDisplayManager.setBrightness(display.getDisplayId(), brightness);
        mA11yMenuLayout.showSnackbar(
                getString(R.string.brightness_percentage_label,
                        (gamma / (BrightnessUtils.GAMMA_SPACE_MAX / 100))));
    }

    private void adjustVolume(int direction) {
        mAudioManager.adjustStreamVolume(
                AudioManager.STREAM_MUSIC, direction,
                AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
        final int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
        final int volume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
        mA11yMenuLayout.showSnackbar(
                getString(
                        R.string.music_volume_percentage_label,
                        (int) (100.0 / maxVolume * volume))
        );
    }

    private void startActivityIfIntentIsSafe(Intent intent, int flag) {
        PackageManager packageManager = getPackageManager();
        List<ResolveInfo> activities = packageManager.queryIntentActivities(intent,
                PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY));
        if (!activities.isEmpty()) {
            intent.setFlags(flag);
            startActivity(intent);
        }
    }

    @Override
    public void onInterrupt() {
    }

    @Override
    public boolean onUnbind(Intent intent) {
        if (sInitialized) {
            unregisterReceiver(mHideMenuReceiver);
            unregisterReceiver(mToggleMenuReceiver);
            mDisplayManager.unregisterDisplayListener(mDisplayListener);
            mPrefs.unregisterOnSharedPreferenceChangeListener(mSharedPreferenceChangeListener);
            sInitialized = false;
        }
        if (mA11yMenuLayout != null) {
            mA11yMenuLayout.clearLayout();
            mA11yMenuLayout = null;
        }
        return super.onUnbind(intent);
    }

    @Override
    protected boolean onKeyEvent(KeyEvent event) {
        if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
            mA11yMenuLayout.hideMenu();
        }
        return false;
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
            if (mA11yMenuLayout.hideMenu()) {
                mLastTimeTouchedOutside = SystemClock.uptimeMillis();
            }
        }
        return false;
    }

    private void toggleVisibility() {
        boolean locked = mKeyguardManager != null && mKeyguardManager.isKeyguardLocked();
        if (!locked) {
            if (SystemClock.uptimeMillis() - mLastTimeTouchedOutside
                    > BUTTON_CLICK_TIMEOUT) {
                mA11yMenuLayout.toggleVisibility();
            }
        } else {
            // Broadcast for testing.
            Intent intent = new Intent(INTENT_OPEN_BLOCKED);
            intent.setPackage(PACKAGE_NAME + PACKAGE_TESTS);
            sendBroadcast(intent);
        }
    }
}

AccessibilityMenuService.java 是 Android 系统中 无障碍菜单(Accessibility Menu) 的核心实现类。这个菜单通常被称为“大按钮菜单”,它为不便使用实体按键或复杂手势的用户提供了一个大型屏显菜单,用来控制设备。

它位于 com.android.systemui 包下,属于 SystemUI 的一部分,这意味着它是 Android 核心系统界面层的组件。

以下是对该类的详细技术解析:

1. 核心定位:它是 AccessibilityService 的子类

AccessibilityMenuService 继承自 android.accessibilityservice.AccessibilityService

  • 权限与安全:作为一个无障碍服务,它必须在 AndroidManifest.xml 中声明 BIND_ACCESSIBILITY_SERVICE 权限,以确保只有系统能绑定它。
  • 功能能力:它通常配置了 canRetrieveWindowContent(获取窗口内容)和 canPerformGestures(执行手势)等权限,以便代表用户执行点击、滑动等操作。

2. 主要职责:系统操作的“中转站”

该服务的主要任务是监听用户的点击操作,并将其转化为全局系统动作。其内部逻辑通常涉及以下功能模块:

  • UI 渲染:负责创建和显示包含“音量”、“亮度”、“最近任务”、“电源”等图标的悬浮层(Overlay)。
  • 动作触发:当用户点击菜单中的某个按钮时,它会调用 performGlobalAction(int action) 方法。
    • 例如:点击“截屏”图标会触发 GLOBAL_ACTION_TAKE_SCREENSHOT
    • 点击“电源”图标会触发 GLOBAL_ACTION_POWER_DIALOG

3. 代码结构关键点分析

根据该文件的源码逻辑,其关键组成部分包括:

A. 生命周期管理 (onServiceConnected)

当用户在设置中开启“无障碍菜单”时,系统会绑定该服务。

@Override
protected void onServiceConnected() {
    // 1. 获取 AccessibilityServiceInfo 配置
    // 2. 初始化用于显示菜单的 WindowManager
    // 3. 注册广播接收器或监听器
}
B. 菜单界面的显示与控制

它通常使用 AccessibilityWindowManager 或类似的辅助类来管理一个 TYPE_ACCESSIBILITY_OVERLAY 类型的窗口。这种窗口能够悬浮在所有应用之上,甚至在锁屏界面也能部分工作。

C. 与 SystemUI 的交互

虽然它是一个无障碍服务,但由于它处于 packages/SystemUI 目录下,它能够更深地访问系统状态。例如:

  • 亮度调节:通过调用系统亮度 API 改变屏幕亮度。
  • 音量控制:与 AudioManager 交互。
  • 通知栏/快捷设置:通过 performGlobalAction(GLOBAL_ACTION_NOTIFICATIONS) 展开通知栏。

4. 为什么它在 SystemUI 目录下?

通常第三方无障碍服务(如 TalkBack)是独立的应用,但 Accessibility Menu 被放在 SystemUI 源码中是因为:

  1. 性能与稳定性:作为系统原生提供的核心辅助功能,需要极高的响应速度。
  2. 视觉一致性:它的样式需要与系统的通知栏、对话框保持风格统一。
  3. 权限深度:SystemUI 拥有最高级别的系统签名,可以处理某些普通无障碍服务难以触达的底层交互。

5. 功能对照表

在该类及其关联的 Layout 中,常见的映射关系如下:

菜单按钮 调用代码 (GlobalAction)
Google 助理 Intent.ACTION_VOICE_COMMAND
辅助功能设置 跳转至无障碍设置 Intent
电源 GLOBAL_ACTION_POWER_DIALOG
调低/调高音量 AudioManager.adjustStreamVolume
最近任务 GLOBAL_ACTION_RECENTS
锁定屏幕 GLOBAL_ACTION_LOCK_SCREEN
快速设置 GLOBAL_ACTION_QUICK_SETTINGS

总结

AccessibilityMenuService.java 是 Android 为了实现数字包容性而设计的关键组件。它将复杂的系统交互简化为直观的、大面积的点击区域。

9. frameworks/base/packages/SystemUI/accessibility/accessibilitymenu/aconfig/Android.bp

目录:
frameworks/base/packages/SystemUI/accessibility/accessibilitymenu/aconfig/Android.bp
链接:
https://cs.android.com/android/platform/superproject/+/android-latest-release:frameworks/base/packages/SystemUI/accessibility/accessibilitymenu/aconfig/Android.bp?hl=zh-cn

package {
    default_team: "trendy_team_system_ui_please_use_a_more_specific_subteam_if_possible_",
}

aconfig_declarations {
    name: "com_android_a11y_menu_flags",
    package: "com.android.systemui.accessibility.accessibilitymenu",
    container: "system_ext",
    srcs: [
        "accessibility.aconfig",
    ],
}

java_aconfig_library {
    name: "com_android_a11y_menu_flags_lib",
    aconfig_declarations: "com_android_a11y_menu_flags",
}

这个 Android.bp 文件位于 Android 系统的 SystemUI 模块中,专门用于管理辅助功能菜单(Accessibility Menu)特性配置(Feature Flags)

在 Android 开发中,aconfig 是谷歌引入的一套新的功能开关(Flag)管理系统。它允许开发者在编译时或运行时动态地开启、关闭或调整某些功能,而不需要频繁修改大量代码。

以下是对该文件涉及的核心概念和结构的详细解释:

1. 核心组件:aconfig

aconfig 是 Android 平台用于管理“特性启动标志(Feature Launch Flags)”的工具。

  • 目的:解决传统硬编码开关难以维护的问题。
  • 工作流:开发者在 .aconfig 文件中定义标志 →\rightarrow Android.bp 声明编译规则 →\rightarrow 自动生成 Java/C++ 代码供业务逻辑调用。

2. 文件内容结构分解

通常这个目录下的 Android.bp 会包含以下三类模块:

A. aconfig_declarations(标志声明)

这是定义的源头。它告诉构建系统去哪里找标志的定义文件。

aconfig_declarations {
    name: "com_android_systemui_accessibility_menu_flags", // 模块名称
    package: "com.android.systemui.accessibility.menu",   // Java 中的包名
    srcs: [
        "accessibility_menu.aconfig", // 包含具体 flag 定义的 textproto 文件
    ],
}
  • 作用:将 .aconfig 源码文件打包成一个中间声明模块。
B. java_aconfig_library(生成 Java 库)

为了让 SystemUI 的 Java 代码能够使用这些标志,必须生成一个 Java 库。

java_aconfig_library {
    name: "com_android_systemui_accessibility_menu_flags_lib",
    aconfig_declarations: "com_android_systemui_accessibility_menu_flags",
}
  • 作用:根据上面的声明,自动生成 Java 类(通常是 Flags.java)。
  • 用法:在 SystemUI 代码中,你可以直接调用 Flags.newFeatureEnabled() 来判断功能是否开启。
C. aconfig_values(标志赋值)

有时需要为不同的“发布版本(Release Config)”设置不同的默认值(例如:在开发版中开启,在稳定版中关闭)。

  • 作用:引用 .textproto 文件来覆盖默认的 flag 值。

3. 该特定路径的背景:Accessibility Menu

该文件所在的路径 frameworks/base/packages/SystemUI/accessibility/accessibilitymenu/ 对应的是 Android 的辅助功能菜单

这是一个为有障碍人士设计的浮动菜单,可以控制音量、亮度、截屏等。通过 aconfig,Android 团队可以:

  • 逐步推出新功能:例如,为辅助功能菜单增加一个新的“快捷手势”,先通过 flag 隐藏,仅对测试机开启。
  • 快速回滚:如果新功能导致 SystemUI 崩溃,可以通过服务器端配置(由 aconfig 支持的运行时下发)直接关闭该功能,无需推送系统更新。

总结

Android.bp 文件是辅助功能菜单功能开关的“编译蓝图”。它负责:

  1. 扫描 定义了哪些新功能开关(.aconfig 文件)。
  2. 转换 为 SystemUI 可以直接调用的 Java 代码库。
  3. 配置 不同版本下这些开关的默认状态。

如果你在修改 Accessibility Menu 的功能,通常需要在这个目录下新建或修改 .aconfig 文件,并在代码中通过生成的库来引用对应的 Flag。

10. accessibility.aconfig

目录:frameworks/base/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig
链接:

package: "com.android.systemui.accessibility.accessibilitymenu"
container: "system_ext"

# NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors.

flag {
    name: "a11y_menu_hide_before_taking_action"
    namespace: "accessibility"
    description: "Hides the AccessibilityMenuService UI before taking action instead of after."
    bug: "292020123"
}

flag {
    name: "action_bar_wrap_content"
    namespace: "accessibility"
    description: "Applies WRAP_CONTENT to the action bar in A11yMenu settings to better fit large fonts"
    bug: "347911378"
    metadata {
        purpose: PURPOSE_BUGFIX
    }
}

flag {
    name: "hide_restricted_actions"
    namespace: "accessibility"
    description: "Hides shortcut buttons for possibly restricted actions like brightness/volume adjustment"
    bug: "347269196"
    metadata {
        purpose: PURPOSE_BUGFIX
    }
}

这个文件位于 SystemUI 的“无障碍菜单”(Accessibility Menu)模块中。

简单来说,这个文件是 Android 14/15 引入的新一代 功能开关(Feature Flags) 定义文件,用于控制无障碍菜单中某些新功能或重构代码的启用与关闭。

以下是该文件的详细解析:

1. 什么是 aconfig

在 Android 开发中,aconfig 是 Google 推出的一套新的功能标志系统(Feature Flagging System)。

  • 目的:它允许开发者在代码中加入新功能,但通过开关来控制这些功能是否在最终镜像中激活。
  • 优势:可以实现“无代码更改的发布”(可以在不重新编译的情况下切换功能状态),并支持根据不同配置(如不同机型或 Android 版本)动态开启功能。

2. 文件路径分析

frameworks/base/packages/SystemUI/accessibility/accessibilitymenu/aconfig/accessibility.aconfig

  • SystemUI: 负责 Android 系统界面的核心包(如下拉通知栏、状态栏等)。
  • accessibility/accessibilitymenu: 专门负责“无障碍菜单”。这是一个悬浮在屏幕上的大图标菜单,方便用户单手操作或替代物理按键(如调节音量、关机、截屏)。

3. 该文件的具体内容及含义

虽然具体代码行会随版本更新,但此类 .aconfig 文件的结构通常如下:

package: "com.android.systemui.accessibility.accessibilitymenu"
container: "system"

flag {
    name: "new_feature_name"
    namespace: "accessibility"
    description: "描述该开关的作用"
    bug: "关联的 Google Issue Tracker ID"
}

在该文件中常见的开关(Flag)可能涉及:

  • UI 重构:例如控制无障碍菜单是否使用新的图标样式或布局。
  • 新动作支持:例如是否允许通过无障碍菜单执行新的快捷操作。
  • 性能优化:控制是否启用某些新的加载逻辑,以减少对系统资源的占用。

4. 为什么这个文件很重要?

  1. 功能解耦:开发者可以将正在开发的实验性功能代码合并进主分支,而不必担心这些未完成的功能会影响正式版用户,因为默认情况下开关是关闭的。
  2. 灰度测试:Google 或厂商可以远程开启某个标志(Flag),先在一小部分用户中测试无障碍菜单的新特性。
  3. 回滚机制:如果新版无障碍菜单出现崩溃或 Bug,只需关闭开关即可瞬间回到旧版本逻辑,无需发布系统更新包。

5. 开发者如何使用它?

当这个 .aconfig 文件定义了一个名为 example_flag 的标志后,Android 构建系统会自动生成对应的 Java/Kotlin 类(通常是 Flags.java)。
在代码中,开发者会这样写:

if (Flags.exampleFlag()) {
    // 执行新功能逻辑
} else {
    // 执行旧逻辑
}

总结

你看到的这个文件是 Android 无障碍菜单功能的“遥控器面板”。它定义了哪些新特性是可以动态控制的。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐