上次我们研究了常态显示下的状态栏,这篇我们来研究下拉后状态栏,页面是status_bar_expanded.xml
我们将下拉后的状态栏拆分来看,首先看QS快捷控制面板
关于QS快捷键我们可以分为两个类型stock和tileservice,stock是在源码中进行添加,tileService则是android7.0时谷歌添加的一个专门可以将第三方应用显示在QS快捷面板中的api,类似红包助手之类的,跟第三应用添加到设置中逻辑大致类似,通过在activity在清单文件中配置对应的action来搜索获取数据,然后控制点击事件和其它一些内容则是继承了TileService的类
stock类型的数据获取是在QSTileHost中获取,QSTileHost在创建的时候会执行TunerServiceImpl类中的addTunable方法,我们看一下这个方法的源码

//TunerServiceImpl
private void addTunable(Tunable tunable, String key) {
        ...
        //注册内容观察者
        Uri uri = Settings.Secure.getUriFor(key);
        Log.d("ssss","uri = "+uri);
        if (!mListeningUris.containsKey(uri)) {
            mListeningUris.put(uri, key);
            mContentResolver.registerContentObserver(uri, false, mObserver, mCurrentUser);
        }
        //通过SettingsProvider来获取QS列表
        String value = Settings.Secure.getStringForUser(mContentResolver, key, mCurrentUser);
        //调用onTuningChanged方法获取QS快捷面板数据
        tunable.onTuningChanged(key, value);
    }
 //QSTileHost
 @Override
    public void onTuningChanged(String key, String newValue) {
       	...
        final List<String> tileSpecs = loadTileSpecs(mContext, newValue);
        int currentUser = ActivityManager.getCurrentUser();
        if (tileSpecs.equals(mTileSpecs) && currentUser == mCurrentUser) return;
        mTiles.entrySet().stream().filter(tile -> !tileSpecs.contains(tile.getKey())).forEach(
                tile -> {
                    if (DEBUG) Log.d(TAG, "Destroying tile: " + tile.getKey());
                    tile.getValue().destroy();
                });
        final LinkedHashMap<String, QSTile> newTiles = new LinkedHashMap<>();
        for (String tileSpec : tileSpecs) {
            QSTile tile = mTiles.get(tileSpec);
            if (tile != null && (!(tile instanceof CustomTile)
                    || ((CustomTile) tile).getUser() == currentUser)) {
                if (tile.isAvailable()) {
                    if (DEBUG) Log.d(TAG, "Adding " + tile);
                    tile.removeCallbacks();
                    if (!(tile instanceof CustomTile) && mCurrentUser != currentUser) {
                        tile.userSwitch(currentUser);
                    }
                    newTiles.put(tileSpec, tile);
                } else {
                    tile.destroy();
                }
            } else {
                if (DEBUG) Log.d(TAG, "Creating tile: " + tileSpec);
                try {
                    tile = createTile(tileSpec);
                    if (tile != null) {
                        if (tile.isAvailable()) {
                            tile.setTileSpec(tileSpec);
                            newTiles.put(tileSpec, tile);
                        } else {
                            tile.destroy();
                        }
                    }
                } catch (Throwable t) {
                    Log.w(TAG, "Error creating tile for spec: " + tileSpec, t);
                }
            }
        }
       	...
    }
    protected List<String> loadTileSpecs(Context context, String tileList) {
        final Resources res = context.getResources();
        //获取QS快捷面板里的需要显示的内容
        String defaultTileList = res.getString(R.string.freeme_quick_settings_tiles);
       	...
        final ArrayList<String> tiles = new ArrayList<String>();
        boolean addedDefault = false;
        for (String tile : tileList.split(",")) {
            tile = tile.trim();
            if (tile.isEmpty()) continue;
            if (tile.equals("default")) {
                if (!addedDefault) {
                    tiles.addAll(Arrays.asList(defaultTileList.split(",")));
                    addedDefault = true;
                }
            } else {
                tiles.add(tile);
            }
        }
        return tiles;
    }
    public QSTile createTile(String tileSpec) {
        for (int i = 0; i < mQsFactories.size(); i++) {
        	//创建tile
            QSTile t = mQsFactories.get(i).createTile(tileSpec);
            if (t != null) {
                return t;
            }
        }
        // M: @ {
        if (mQuickSettingsExt != null && mQuickSettingsExt.doOperatorSupportTile(tileSpec)) {
            // WifiCalling
            return (QSTile) mQuickSettingsExt.createTile(this, tileSpec);
        }
        // @ }
        return null;
    }

创建Tile是再QSFactoryImpl类里

public QSTile createTile(String tileSpec) {
        QSTileImpl tile = createTileInternal(tileSpec);
        if (tile != null) {
            tile.handleStale(); // Tile was just created, must be stale.
        }
        return tile;
    }

    private QSTileImpl createTileInternal(String tileSpec) {
        /// M: Add extra tiles in quicksetting @{
        Context context = mHost.getContext();
        IQuickSettingsPlugin quickSettingsPlugin = OpSystemUICustomizationFactoryBase
                .getOpFactory(context).makeQuickSettings(context);
        /// @}
        // Stock tiles.
        switch (tileSpec) {
            case "wifi":
                return new WifiTile(mHost);
            case "bt":
                return new BluetoothTile(mHost);
            case "cell":
                return new CellularTile(mHost);
            case "dnd":
                return new DndTile(mHost);
            case "inversion":
                return new ColorInversionTile(mHost);
            case "airplane":
                return new AirplaneModeTile(mHost);
            case "work":
                return new WorkModeTile(mHost);
            case "rotation":
                return new RotationLockTile(mHost);
            case "flashlight":
                return new FlashlightTile(mHost);
            case "location":
                return new LocationTile(mHost);
            case "cast":
                return new CastTile(mHost);
            case "hotspot":
                return new HotspotTile(mHost);
            case "user":
                return new UserTile(mHost);
            case "battery":
                return new BatterySaverTile(mHost);
            case "saver":
                return new DataSaverTile(mHost);
            case "night":
                return new NightDisplayTile(mHost);
            case "nfc":
                return new NfcTile(mHost);
        }
		...
        return null;
    }

加载流程

关于显示的图标及标签,以蓝牙BluetoothTile为例,标签和图标是在

@Override
    protected void handleUpdateState(BooleanState state, Object arg) {
        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();
        }
        /*/ freeme.gouzhouping, 20180117. FreemeAppTheme, qs container.
        state.slash.isSlashed = !enabled;
        //*/
        state.label = mContext.getString(R.string.quick_settings_bluetooth_label);
        state.secondaryLabel = TextUtils.emptyIfNull(
                getSecondaryLabel(enabled, connecting, connected, state.isTransient));
        if (enabled) {
            if (connected) {
                /*/ freeme.gouzhouping, 20180117. FreemeAppTheme, qs container.
                state.icon = new BluetoothConnectedTileIcon();
                /*/
                state.icon = ResourceIcon.get(R.drawable.freeme_ic_qs_bluetooth);
                //*/
                if (!TextUtils.isEmpty(mController.getConnectedDeviceName())) {
                    state.label = mController.getConnectedDeviceName();
                }
                state.contentDescription =
                        mContext.getString(R.string.accessibility_bluetooth_name, state.label)
                                + ", " + state.secondaryLabel;
            } else if (state.isTransient) {
                /*/ freeme.gouzhouping, 20180117. FreemeAppTheme, qs container.
                state.icon = ResourceIcon.get(R.drawable.ic_bluetooth_transient_animation);
                /*/
                state.icon = ResourceIcon.get(R.drawable.freeme_ic_qs_bluetooth);
                //*/
                state.contentDescription = state.secondaryLabel;
            } else {
                /*/ freeme.gouzhouping, 20180117. FreemeAppTheme, qs container.
                state.icon = ResourceIcon.get(R.drawable.ic_qs_bluetooth_on);
                /*/
                state.icon = ResourceIcon.get(R.drawable.freeme_ic_qs_bluetooth);
                //*/
                state.contentDescription = mContext.getString(
                        R.string.accessibility_quick_settings_bluetooth) + ","
                        + mContext.getString(R.string.accessibility_not_connected);
            }
            state.state = Tile.STATE_ACTIVE;
        } else {
            /*/ freeme.gouzhouping, 20180117. FreemeAppTheme, qs container.
            state.icon = ResourceIcon.get(R.drawable.ic_qs_bluetooth_on);
            /*/
            state.icon = ResourceIcon.get(R.drawable.freeme_ic_qs_bluetooth);
            //*/
            state.contentDescription = mContext.getString(
                    R.string.accessibility_quick_settings_bluetooth);
            state.state = Tile.STATE_INACTIVE;
        }

        state.dualLabelContentDescription = mContext.getResources().getString(
                R.string.accessibility_quick_settings_open_settings, getTileLabel());
        state.expandedAccessibilityClassName = Switch.class.getName();
    }

进行设置state.label值为标签,state.icon为图标
关于图标颜色及大小控制,根据方法追踪,最后发现是在QSIconViewImpl中控制

public QSIconViewImpl(Context context) {
        super(context);

        final Resources res = context.getResources();
        //*/ freeme.songyan, 20190103. simple launcher.
        mIconSizePx = res.getDimensionPixelSize(SimpleLauncherObserver.getInstance(mContext).isShowMode() ?
                R.dimen.qs_tile_icon_size_simple_launcher : R.dimen.qs_tile_icon_size);
        /*/
        mIconSizePx = res.getDimensionPixelSize(R.dimen.qs_tile_icon_size);
        //*/
        mTilePaddingBelowIconPx =  res.getDimensionPixelSize(R.dimen.qs_tile_padding_below_icon);

        mIcon = createIcon();
        addView(mIcon);
    }
    
 	private static final int TINT_COLOR_ON  = R.color.freeme_tile_icon_on;
    private static final int TINT_COLOR_OFF = R.color.freeme_tile_icon_off;
    private static final int TINT_COLOR_UNAVAILABLE = R.color.freeme_tile_icon_unavailable;
    
    private void tintColor(Context context, State state, Drawable d) {
        if (state.state == Tile.STATE_UNAVAILABLE) {
            d.setTint(context.getResources().getColor(TINT_COLOR_UNAVAILABLE));
        } else {
            d.setTint(context.getResources().getColor(
                    (state != null && state instanceof QSTile.BooleanState && ((QSTile.BooleanState) state).value)
                            || (state != null && state.state == Tile.STATE_ACTIVE)
                            ? TINT_COLOR_ON : TINT_COLOR_OFF
            ));
        }
    }

同样,关于字体颜色及大小的控制是在QSTileView中

public QSTileView(Context context, QSIconView icon, boolean collapsedView) {
        super(context, icon, collapsedView);
        ...
        FontSizeUtils.updateFontSize(mLabel, SimpleLauncherObserver.getInstance(mContext).isShowMode()
                ? R.dimen.qs_tile_text_size_simple_launcher : R.dimen.qs_tile_text_size);
    }
    
	private final int TINT_COLOR_ON  = getContext().getColor(R.color.freeme_tile_label_on);
    private final int TINT_COLOR_OFF = getContext().getColor(R.color.freeme_tile_label_off);
    
    private void tintColor(QSTile.State state, TextView tv) {
        tv.setTextColor(
                (state != null && state instanceof QSTile.BooleanState && ((QSTile.BooleanState) state).value)
                        || (state != null && state.state == Tile.STATE_ACTIVE)
                        ? TINT_COLOR_ON : TINT_COLOR_OFF
        );
        if (state.dualTarget) {
            ((ImageView) mExpandIndicator).setColorFilter((state != null && state instanceof QSTile.BooleanState
                    && ((QSTile.BooleanState) state).value)
                    || (state != null && state.state == Tile.STATE_ACTIVE)
                    ? TINT_COLOR_ON : TINT_COLOR_OFF);
            ((ImageView) mExpandIndicator).invalidate();
        }
    }

我们看一下stock类型是如何跟任务栏中的图标互通的,例如蓝牙,我们看一下BluetoothTile中的内容

//点击事件
	@Override
    protected void handleClick() {
        // Secondary clicks are header clicks, just toggle.
        //获取当前状态
        final boolean isEnabled = mState.value;
        // Immediately enter transient enabling state when turning bluetooth on.
        //更改状态
        refreshState(isEnabled ? null : ARG_SHOW_TRANSIENT_ENABLING);
        //与任务栏中的图标和设置内的内容互通
        mController.setBluetoothEnabled(!isEnabled);
    }

refreshState方法最终会调用上面我们说过的handleUpdateState方法,根据点击后的状态来修改对应的样式,我们再看一下mController.setBluetoothEnabled,这个方法BluetoothControllerImpl中

@Override
    public void setBluetoothEnabled(boolean enabled) {
        if (mLocalBluetoothManager != null) {
            mLocalBluetoothManager.getBluetoothAdapter().setBluetoothEnabled(enabled);
        }
    }

蓝牙的流程是通过控制器获取adapter然后再将值传入IBander进行跨进程通信,QS快捷面板跟设置进行互通最主要的就是IBander,其它的只是辅助的内容
与任务栏中的图标互通,首先PhoneStatusBarPolicy实现了蓝牙的接口,并且进行添加,然后BluetoothEventManager里通过广播监听蓝牙的改变,然后将改变后的数据通过接口传递给BluetoothControllerImpl,BluetoothControllerImpl在创建的时候就实现了接口并添加,BluetoothControllerImpl在接受到数据后再调用接口,将改变的数据传递给PhoneStatusBarPolicy进行图标的显示与修改

//PhoneStatusBarPolicy
	@VisibleForTesting
    public PhoneStatusBarPolicy(Context context, StatusBarIconController iconController) {
		...
		mBluetooth = Dependency.get(BluetoothController.class);
		...
		updateBluetooth();
		...
		mBluetooth.addCallback(this);
	}

//BluetoothControllerImpl
	public BluetoothControllerImpl(Context context, Looper bgLooper) {
        mLocalBluetoothManager = Dependency.get(LocalBluetoothManager.class);
        mBgHandler = new Handler(bgLooper);
        if (mLocalBluetoothManager != null) {
            mLocalBluetoothManager.getEventManager().setReceiverHandler(mBgHandler);
            //添加监听
            mLocalBluetoothManager.getEventManager().registerCallback(this);
            mLocalBluetoothManager.getProfileManager().addServiceListener(this);
            onBluetoothStateChanged(
                    mLocalBluetoothManager.getBluetoothAdapter().getBluetoothState());
        }
        mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
        mCurrentUser = ActivityManager.getCurrentUser();
    }
    
	@Override
    public void onBluetoothStateChanged(int bluetoothState) {
        mEnabled = bluetoothState == BluetoothAdapter.STATE_ON
                || bluetoothState == BluetoothAdapter.STATE_TURNING_ON;
        if (DEBUG) {
            Log.d(TAG, "onBluetoothStateChanged, bluetoothState = " + bluetoothState
                    + ", mEnabled = " + mEnabled);
        }
        mState = bluetoothState;
        updateConnected();
        //调用接口
        mHandler.sendEmptyMessage(H.MSG_STATE_CHANGED);
    }

	private final class H extends Handler {
        private final ArrayList<BluetoothController.Callback> mCallbacks = new ArrayList<>();

        private static final int MSG_PAIRED_DEVICES_CHANGED = 1;
        private static final int MSG_STATE_CHANGED = 2;
        private static final int MSG_ADD_CALLBACK = 3;
        private static final int MSG_REMOVE_CALLBACK = 4;

        public H(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                ...
                case MSG_STATE_CHANGED:
                    fireStateChange();
                    break;
                case MSG_ADD_CALLBACK:
                    mCallbacks.add((BluetoothController.Callback) msg.obj);
                    break;
                case MSG_REMOVE_CALLBACK:
                    mCallbacks.remove((BluetoothController.Callback) msg.obj);
                    break;
            }
        }
        
        private void fireStateChange() {
            for (BluetoothController.Callback cb : mCallbacks) {
                fireStateChange(cb);
            }
        }
        
        private void fireStateChange(BluetoothController.Callback cb) {
        	//调用接口
            cb.onBluetoothStateChange(mEnabled);
        }
    }

//BluetoothEventManager
	//添加监听
	public void registerCallback(BluetoothCallback callback) {
        synchronized (mCallbacks) {
            mCallbacks.add(callback);
        }
    }
    
	private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            BluetoothDevice device = intent
                    .getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);

            Handler handler = mHandlerMap.get(action);
            if (handler != null) {
                handler.onReceive(context, intent, device);
            }
        }
    };
    
    private class AdapterStateChangedHandler implements Handler {
        public void onReceive(Context context, Intent intent,
                BluetoothDevice device) {
            int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
                                    BluetoothAdapter.ERROR);
            // Reregister Profile Broadcast Receiver as part of TURN OFF
            if (state == BluetoothAdapter.STATE_OFF)
            {
                context.unregisterReceiver(mProfileBroadcastReceiver);
                registerProfileIntentReceiver();
            }
            // update local profiles and get paired devices
            mLocalAdapter.setBluetoothStateInt(state);
            // send callback to update UI and possibly start scanning
            synchronized (mCallbacks) {
                for (BluetoothCallback callback : mCallbacks) {
                    //调用接口
                    callback.onBluetoothStateChanged(state);
                }
            }
            // Inform CachedDeviceManager that the adapter state has changed
            mDeviceManager.onBluetoothStateChanged(state);
        }
    }

我们看上面的逻辑可以发现一个问题,BluetoothControllerImpl添加接口和调用接口都是再Handler里进行,为什么?因为线程问题,我们都知道耗时操作在子线程中进行,而UI的修改必须在主线程中进行,所以需要通过Handler跳回主线程,所以在添加Tile的时候需要特别注意一下
接下来说一下QS的编辑功能,QS编辑页面是一个自定义的LinearLayout控件,页面布局是freeme_qs_customize_panel_content
编辑按钮是在qs_footer_impl.xml布局中,点击事件是在QSFooterImpl中设置,点击后会调用QSPanel中showEdit方法,再触发QSCustomizer中show方法,然后进行数据加载及页面加载
QS编辑页面的列表是一个RecyclerView,数据是在TileQueryHelper中的addStockTiles方法获取源码中添加的Tile,addPackageTiles方法获取通过TileService添加的Tile,通过TileAdapter加载列表
拖动功能则是通过ItemTouchHelper来实现

private final ItemTouchHelper.Callback mCallbacks = new ItemTouchHelper.Callback() {
       ...
        @Override
        public void onSelectedChanged(ViewHolder viewHolder, int actionState) {
            super.onSelectedChanged(viewHolder, actionState);
            //判断当前是长按选择还是松开
            if (actionState != ItemTouchHelper.ACTION_STATE_DRAG) {
                viewHolder = null;
            }
            if (viewHolder == mCurrentDrag) return;
            if (mCurrentDrag != null) {
                int position = mCurrentDrag.getAdapterPosition();
                TileInfo info = mTiles.get(position);
                mCurrentDrag.mTileView.setShowAppLabel(
                        position > mEditIndex && !info.isSystem);
                //Tile松开动画
                mCurrentDrag.stopDrag();
                mCurrentDrag = null;
            }
            if (viewHolder != null) {
                mCurrentDrag = (Holder) viewHolder;
                //Tile长按选中动画
                mCurrentDrag.startDrag();
            }
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                	//刷新页面,主要是刷新提示语句
                    notifyItemChanged(mEditIndex);
                }
            });
        }

        @Override
        public boolean canDropOver(RecyclerView recyclerView, ViewHolder current,
                ViewHolder target) {
            //判断是否可以交换位置
            if (!canRemoveTiles() && current.getAdapterPosition() < mEditIndex) {
                return target.getAdapterPosition() < mEditIndex;
            }
            return target.getAdapterPosition() <= mEditIndex + 1;
        }

        @Override
        public int getMovementFlags(RecyclerView recyclerView, ViewHolder viewHolder) {
        //设置ViewHolder的拖动和滑动方向
            if (viewHolder.getItemViewType() == TYPE_EDIT || viewHolder.getItemViewType() == TYPE_DIVIDER) {
                return makeMovementFlags(0, 0);
            }
            int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.RIGHT
                    | ItemTouchHelper.LEFT;
            return makeMovementFlags(dragFlags, 0);
        }

        @Override
        public boolean onMove(RecyclerView recyclerView, ViewHolder viewHolder, ViewHolder target) {
            //两个viewHolder交换时触发
            int from = viewHolder.getAdapterPosition();
            int to = target.getAdapterPosition();
            return move(from, to, target.itemView);
        }
        ...
    };

当我们编辑完成后会执行TileAdapter中的saveSpecs方法进行保存

//TileAdapter
public void saveSpecs(QSTileHost host) {
        List<String> newSpecs = new ArrayList<>();
        for (int i = 0; i < mTiles.size() && mTiles.get(i) != null; i++) {
            newSpecs.add(mTiles.get(i).spec);
        }
        host.changeTiles(mCurrentSpecs, newSpecs);
        mCurrentSpecs = newSpecs;
    }
//QSTileHost
public void changeTiles(List<String> previousTiles, List<String> newTiles) {
        ...
        if (DEBUG) Log.d(TAG, "saveCurrentTiles " + newTiles);
        //保存到SettingsProvider中
        Secure.putStringForUser(getContext().getContentResolver(), QSTileHost.TILES_SETTING,
                TextUtils.join(",", newTiles), ActivityManager.getCurrentUser());
    }

在上面我们说过执行addTunable时会注册一个内容观察者,每此保存数据后都会触发内容观察者,执行reloadSetting方法刷新QS快捷面板