Android开发Android进阶之路

Android 13 蓝牙等快捷开关更新流程

2022-12-09  本文已影响0人  孤街酒客0911

学习笔记:


在这篇文章之前,有时候就纠结:快捷面板的点击事件是怎样的。

点击后快捷面板图标怎么更改?

下面以蓝牙 BluetoothTile 为例进行分析。

BluetoothTile

// BluetoothTile.java

public class BluetoothTile extends QSTileImpl<BooleanState> {
    private static final Intent BLUETOOTH_SETTINGS = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);

    private final BluetoothController mController;

    @Inject
    public BluetoothTile(
            QSHost host,
            @Background Looper backgroundLooper,
            @Main Handler mainHandler,
            FalsingManager falsingManager,
            MetricsLogger metricsLogger,
            StatusBarStateController statusBarStateController,
            ActivityStarter activityStarter,
            QSLogger qsLogger,
            BluetoothController bluetoothController
    ) {
        super(host, backgroundLooper, mainHandler, falsingManager, metricsLogger,
                statusBarStateController, activityStarter, qsLogger);
        mController = bluetoothController;
        mController.observe(getLifecycle(), mCallback);
    }

    @Override
    public BooleanState newTileState() {
        return new BooleanState();
    }

    // 处理点击事件;打开/关闭的动画也在这里调用;
    @Override
    protected void handleClick(@Nullable View view) {
        // 获取蓝牙此时的状态
        final boolean isEnabled = mState.value;
        // 刷新视图状态
        refreshState(isEnabled ? null : ARG_SHOW_TRANSIENT_ENABLING);
        // 设置蓝牙状态
        mController.setBluetoothEnabled(!isEnabled);
    }

    @Override
    public Intent getLongClickIntent() {
        // 返回长按的意图,
        return new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
    }
    // 目前不知道,没调用这里
    @Override
    protected void handleSecondaryClick(@Nullable View view) {
        if (!mController.canConfigBluetooth()) {
            mActivityStarter.postStartActivityDismissingKeyguard(
                    new Intent(Settings.ACTION_BLUETOOTH_SETTINGS), 0);
            return;
        }
        if (!mState.value) {
            mController.setBluetoothEnabled(true);
        }
    }
    // 返回 title 名字
    @Override
    public CharSequence getTileLabel() {
        return mContext.getString(R.string.quick_settings_bluetooth_label);
    }

    // 处理更新状态,点击后会回调到这里
    @Override
    protected void handleUpdateState(BooleanState state, Object arg) {
        // 省略部分代码......
    }

     // 获取 二级标签 描述
    @Nullable
    private String getSecondaryLabel(boolean enabled, boolean connecting, boolean connected,
            boolean isTransient) {
        if (connecting) {
            return mContext.getString(R.string.quick_settings_connecting);
        }
        if (isTransient) {
            return mContext.getString(R.string.quick_settings_bluetooth_secondary_label_transient);
        }

        List<CachedBluetoothDevice> connectedDevices = mController.getConnectedDevices();
        if (enabled && connected && !connectedDevices.isEmpty()) {

                // 省略部分代码......
            } else {
                // 省略部分代码......
            }
        }

        return null;
    }

    @Override
    public int getMetricsCategory() {
        return MetricsEvent.QS_BLUETOOTH;
    }
    // 是否可用
    @Override
    public boolean isAvailable() {
        return mController.isBluetoothSupported();
    }

    // 蓝牙状态更改回调
    private final BluetoothController.Callback mCallback = new BluetoothController.Callback() {
        @Override
        public void onBluetoothStateChange(boolean enabled) {
            refreshState();
        }

        @Override
        public void onBluetoothDevicesChanged() {
            refreshState();
        }
    };

    // 蓝牙连接时的图标
    private class BluetoothConnectedTileIcon extends Icon {

        BluetoothConnectedTileIcon() {
            // Do nothing. Default constructor to limit visibility.
        }

        @Override
        public Drawable getDrawable(Context context) {
            // This method returns Pair<Drawable, String> - the first value is the drawable.
            return context.getDrawable(R.drawable.ic_bluetooth_connected);
        }
    }
}

BluetoothTile 主要还是实现了 QSTileImpl里面的一些抽象方法:

// QSTileImpl.java

    /**
     * Provides a new {@link TState} of the appropriate type to use between this tile and the
     * corresponding view.
     *
     * @return new state to use by the tile.
     */
    public abstract TState newTileState();

    /**
     * Handles clicks by the user.
     *
     * Calls to the controller should be made here to set the new state of the device.
     *
     * @param view The view that was clicked.
     */
    protected abstract void handleClick(@Nullable View view);

    /**
     * Update state of the tile based on device state
     *
     * Called whenever the state of the tile needs to be updated, either after user
     * interaction or from callbacks from the controller. It populates {@code state} with the
     * information to display to the user.
     *
     * @param state {@link TState} to populate with information to display
     * @param arg additional arguments needed to populate {@code state}
     */
    abstract protected void handleUpdateState(TState state, Object arg);

在这里主要分析点击事件,以 handleClick(@Nullable View view) 为例。当点击 title 时,就会调用 QSTileImpl 的 click(@Nullable View view) 方法,通过 Handler 发送消息,

// QSTileImpl.java
    public void click(@Nullable View view) {
        mMetricsLogger.write(populate(new LogMaker(ACTION_QS_CLICK).setType(TYPE_ACTION)
                .addTaggedData(FIELD_STATUS_BAR_STATE,
                        mStatusBarStateController.getState())));
        mUiEventLogger.logWithInstanceId(QSEvent.QS_ACTION_CLICK, 0, getMetricsSpec(),
                getInstanceId());
        mQSLogger.logTileClick(mTileSpec, mStatusBarStateController.getState(), mState.state);
        if (!mFalsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) {
            mHandler.obtainMessage(H.CLICK, view).sendToTarget();
        }
    }



        @Override
        public void handleMessage(Message msg) {
            String name = null;
            try {

                // 省略部分代码......

                 else if (msg.what == CLICK) {
                    name = "handleClick";
                    //  根据状态策略进行判断
                    if (mState.disabledByPolicy) {
                        Intent intent = RestrictedLockUtils.getShowAdminSupportDetailsIntent(
                                mContext, mEnforcedAdmin);
                        mActivityStarter.postStartActivityDismissingKeyguard(intent, 0);
                    } else {
                        // 我们关注这里,这里将回调到 BluetoothTile 里的 handleClick() 方法。
                        handleClick((View) msg.obj);
                    }
                } 
                // 省略部分代码......

            } catch (Throwable t) {
                final String error = "Error in " + name;
                Log.w(TAG, error, t);
                mHost.warn(error, t);
            }
        }

下面一起看 BluetoothTile#handleClick() 方法:

// BluetoothTile.java

    @Override
    protected void handleClick(@Nullable View view) {
        // 获取状态
        final boolean isEnabled = mState.value;
        // 刷新视图
        refreshState(isEnabled ? null : ARG_SHOW_TRANSIENT_ENABLING);
        // 设置蓝牙状态
        mController.setBluetoothEnabled(!isEnabled);
    }

在这里主要关注 refreshState() 方法,该方法是父类 QSTileImpl 的方法:
QSTileImpl#refreshState()

// QSTileImpl.java

    protected final void refreshState(@Nullable Object arg) {
        mHandler.obtainMessage(H.REFRESH_STATE, arg).sendToTarget();
    }

这里又是通过 Handler 发送消息,将会调用里面的 handleRefreshState(msg.obj) 方法;
这里看 QSTileImpl#handleRefreshState() 方法:

// QSTileImpl.java

    protected final void handleRefreshState(@Nullable Object arg) {
        // 这里重点关注,处理状态更新
        handleUpdateState(mTmpState, arg);
        boolean changed = mTmpState.copyTo(mState);
        if (mReadyState == READY_STATE_READYING) {
            mReadyState = READY_STATE_READY;
            changed = true;
        }
        // 有改变
        if (changed) {
            mQSLogger.logTileUpdated(mTileSpec, mState);
            // 这里重点关注,处理状态改变
            handleStateChanged();
        }
        // 省略部分代码......
    }

handleUpdateState() 是一个抽象方法给,所以会回调到 BluetoothTile 中的 handleUpdateState() 方法;
一起看 BluetoothTile#handleUpdateState():

// BluetoothTile.java
    // 该方法就是在点击后 设置图标、描述 等一些状态
    @Override
    protected void handleUpdateState(BooleanState state, Object arg) {
        checkIfRestrictionEnforcedByAdminOnly(state, UserManager.DISALLOW_BLUETOOTH);
        final boolean transientEnabling = arg == ARG_SHOW_TRANSIENT_ENABLING;
        final boolean enabled = transientEnabling || mController.isBluetoothEnabled();
        final boolean connected = mController.isBluetoothConnected();
        final boolean connecting = mController.isBluetoothConnecting();
        state.isTransient = transientEnabling || connecting ||
                mController.getBluetoothState() == BluetoothAdapter.STATE_TURNING_ON;
        state.dualTarget = true;
        state.value = enabled;
        if (state.slash == null) {
            state.slash = new SlashState();
        }
        state.slash.isSlashed = !enabled;
        state.label = mContext.getString(R.string.quick_settings_bluetooth_label);
        state.secondaryLabel = TextUtils.emptyIfNull(
                getSecondaryLabel(enabled, connecting, connected, state.isTransient));
        state.contentDescription = mContext.getString(
                R.string.accessibility_quick_settings_bluetooth);
        state.stateDescription = "";
        if (enabled) {
            if (connected) {
                state.icon = new BluetoothConnectedTileIcon();
                if (!TextUtils.isEmpty(mController.getConnectedDeviceName())) {
                    state.label = mController.getConnectedDeviceName();
                }
                state.stateDescription =
                        mContext.getString(R.string.accessibility_bluetooth_name, state.label)
                                + ", " + state.secondaryLabel;
            } else if (state.isTransient) {
                state.icon = ResourceIcon.get(
                        com.android.internal.R.drawable.ic_bluetooth_transient_animation);
                state.stateDescription = state.secondaryLabel;
            } else {
                state.icon =
                        ResourceIcon.get(com.android.internal.R.drawable.ic_qs_bluetooth);
                state.stateDescription = mContext.getString(R.string.accessibility_not_connected);
            }
            state.state = Tile.STATE_ACTIVE;
        } else {
            state.icon = ResourceIcon.get(com.android.internal.R.drawable.ic_qs_bluetooth);
            state.state = Tile.STATE_INACTIVE;
        }
        state.dualLabelContentDescription = mContext.getResources().getString(
                R.string.accessibility_quick_settings_open_settings, getTileLabel());
        state.expandedAccessibilityClassName = Switch.class.getName();
    }

在 QSTileImpl#handleRefreshState() 方法里紧接着会调用 handleStateChanged() 方法。
一起看看 QSTileImpl#handleStateChanged():

// QSTileImpl.java

    private void handleStateChanged() {
        if (mCallbacks.size() != 0) {
            for (int i = 0; i < mCallbacks.size(); i++) {
                // 找到对应的回调,更新状态
                mCallbacks.get(i).onStateChanged(mState);
            }
        }
    }

这个回调将会到 QSPanel 类里面。
一起看看 QSPanel#addTile():

// QSPanel.java

    void addTile(QSPanelControllerBase.TileRecord tileRecord) {
        final QSTile.Callback callback = new QSTile.Callback() {
            @Override
            public void onStateChanged(QSTile.State state) {
                drawTile(tileRecord, state);
            }
        };
        // 添加回调 
        tileRecord.tile.addCallback(callback);
        tileRecord.callback = callback;
        tileRecord.tileView.init(tileRecord.tile);
        tileRecord.tile.refreshState();

        if (mTileLayout != null) {
            mTileLayout.addTile(tileRecord);
        }
    }

addTile() 方法是每个 title 创建时调用的,这里会为每个 title 添加一个 Callback。

当状态改变时,就会回调到上述位置,跟进去将会到 QSTileViewImpl.kt 里的 onStateChanged(state: QSTile.State) 方法。

接着看 QSTileViewImpl#onStateChanged()

// QSTileViewImpl.kt

    override fun onStateChanged(state: QSTile.State) {
        post {
            handleStateChanged(state)
        }
    }

    // 处理状态变化相关方法
    protected open fun handleStateChanged(state: QSTile.State) {

        // 省略部分代码......

        // 设置图标,这里会回调到 QSIconViewImpl.java 里。
        icon.setIcon(state, allowAnimations)
        // 获取 内容说明
        contentDescription = state.contentDescription

        // State handling and description
        val stateDescription = StringBuilder()
        // 状态文本,这里我打印过:已关闭、不可用、已开启 等这种文本。
        val stateText = getStateText(state)
        if (!TextUtils.isEmpty(stateText)) {
            stateDescription.append(stateText)
            if (TextUtils.isEmpty(state.secondaryLabel)) {
                // 设置二级标签 文本;二级标签 应该是 title 的状态
                state.secondaryLabel = stateText
            }
        }
        if (!TextUtils.isEmpty(state.stateDescription)) {
            stateDescription.append(", ")
            stateDescription.append(state.stateDescription)
            if (lastState != INVALID && state.state == lastState &&
                    state.stateDescription != lastStateDescription) {
                stateDescriptionDeltas = state.stateDescription
            }
        }

        setStateDescription(stateDescription.toString())
        lastStateDescription = state.stateDescription

        accessibilityClass = if (state.state == Tile.STATE_UNAVAILABLE) {
            null
        } else {
            state.expandedAccessibilityClassName
        }

        if (state is BooleanState) {
            val newState = state.value
            if (tileState != newState) {
                tileState = newState
            }
        }
        //

        // Labels
        if (!Objects.equals(label.text, state.label)) {
            // 本文是以蓝牙为例,所以这里是:state.labe 是 ‘蓝牙’
            label.text = state.label
        }

        if (!Objects.equals(secondaryLabel.text, state.secondaryLabel)) {
            // 设置 二级标签的文本,蓝牙的二级标签为:‘已关闭’ 或 ‘已开启’
            secondaryLabel.text = state.secondaryLabel
            // 判断二级标签是否显示,并设置状态否显示
            secondaryLabel.visibility = if (TextUtils.isEmpty(state.secondaryLabel)) {
                GONE
            } else {
                VISIBLE
            }
        }

        // 设置颜色,这里我去掉后,所有 title 不管是否被点击,都是白色的。
        if (state.state != lastState) {
            singleAnimator.cancel()
            if (allowAnimations) {
                singleAnimator.setValues(
                        colorValuesHolder(
                                BACKGROUND_NAME,
                                paintColor,
                                getBackgroundColorForState(state.state)
                        ),
                        colorValuesHolder(
                                LABEL_NAME,
                                label.currentTextColor,
                                getLabelColorForState(state.state)
                        ),
                        colorValuesHolder(
                                SECONDARY_LABEL_NAME,
                                secondaryLabel.currentTextColor,
                                getSecondaryLabelColorForState(state.state)
                        ),
                        colorValuesHolder(
                                CHEVRON_NAME,
                                chevronView.imageTintList?.defaultColor ?: 0,
                                getChevronColorForState(state.state)
                        )
                    )
                singleAnimator.start()
            } else {
                setAllColors(
                    getBackgroundColorForState(state.state),
                    getLabelColorForState(state.state),
                    getSecondaryLabelColorForState(state.state),
                    getChevronColorForState(state.state)
                )
            }
        }

        // Right side icon
        loadSideViewDrawableIfNecessary(state)

        label.isEnabled = !state.disabledByPolicy
        // 把当前状态设置成上一状态。
        lastState = state.state
    }

至此分析完成,

留一个问题: QSTileViewImpl#onStateChanged() 方法中设置图标的时 icon.setIcon(state, allowAnimations),这句话 icon 对象是怎么来的?

上一篇下一篇

猜你喜欢

热点阅读