安卓开发- 安卓13 Launcher3 Hotseat、导航栏相关修改

概述

上一篇文章简单分析了Launcher3桌面布局构成、搜索框的修改、桌面应用和组件的修改,这篇文章继续来分析下Hotseat和导航栏的相关修改(主要是介绍Hotseat的相关改动,导航栏只是改动了一下位置)。

阅读此篇文章前,请先了解Launcher3的布局构成等概念,还没有了解Launcher3基本布局以及整体改动思路的小伙伴可以先看下上一篇文章:安卓开发- 安卓13 Launcher3 主页布局修改

HotSeat相关修改

Hotseat简介

​ Hotseat的设计目的是为了方便用户快速访问这些高频应用,而无需返回到主屏幕或通过应用抽屉查找。Hotseat的特点包括:

  • 固定位置:Hotseat在屏幕底部,不随桌面页面的左右滑动而移动,始终可见。
  • 自定义:用户可以根据自己的喜好向Hotseat添加或移除应用图标
  • 可扩展性:在某些Launcher实现中,Hotseat的宽度和包含的图标数量可以根据用户的设置或设备屏幕尺寸进行调整。
  • 创建文件夹:在一些Launcher版本中,用户还可以在Hotseat上创建文件夹,将多个应用图标组织在一起。
  • 禁止拖动功能:某些定制需求可能要求禁用将图标拖动到Hotseat的能力,以满足特定的用户体验或界面设计要求。

另外:

  • Hotseat其实也是使用一个CellLayout负责管理里面的所有数据
  • 大部分配置可以通过XML配置文件修改得到
  • 加载和绑定数据和workspace基本是一致的

​ 开发者在定制Launcher3时,可以通过修改源代码来控制Hotseat的行为,比如禁止拖放图标、调整图标数量、去掉Hotseat功能或改变其位置等,以满足不同设备或用户界面设计的需求。这通常涉及修改DeviceProfile.javaWorkspace.javaCellLayout.java等关键类中的逻辑。

​ 我这里的项目主要是大屏设备,所以使用了6x5的布局来讲解。原生Launcher3 布局图(6x5布局)如下:

在这里插入图片描述

设置默认的Hotseat应用

先在Launcher.xml中确认已存在Hotseat布局:

<?xml version="1.0" encoding="utf-8"?>
<com.android.launcher3.LauncherRootView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:launcher="http://schemas.android.com/apk/res-auto"
    android:id="@+id/launcher"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">
    
    <com.android.launcher3.dragndrop.DragLayer
        android:id="@+id/drag_layer"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:clipChildren="false"
        android:clipToPadding="false"
        android:importantForAccessibility="no">


        <!-- include Hotseat, id必须是hotseat -->
        <include
            android:id="@+id/hotseat"
            layout="@layout/hotseat" />

		<!--其他布局-->

    </com.android.launcher3.dragndrop.DragLayer>

</com.android.launcher3.LauncherRootView>

然后在Launcher3/res/xml/device_profiles.xml中配置Hotseat相关的属性:

<!--Launcher3/res/xml/device_profiles.xml-->
<?xml version="1.0" encoding="utf-8"?>
<profiles xmlns:launcher="http://schemas.android.com/apk/res-auto" >
    <其他的grid-option布局...>
        
    <grid-option
        launcher:name="6_by_5"
        launcher:numRows="6"
        launcher:numColumns="7"
        launcher:numSearchContainerColumns="5"
        launcher:numFolderRows="3"
        launcher:numFolderColumns="4"
        launcher:numHotseatIcons="5"
        launcher:hotseatColumnSpanLandscape="4"
        launcher:numAllAppsColumns="6"
        launcher:isScalable="true"
        launcher:inlineNavButtonsEndSpacing="@dimen/taskbar_button_margin_6_5"
        launcher:devicePaddingId="@xml/paddings_6x5"
        launcher:dbFile="launcher_6_by_5.db"
        launcher:defaultLayoutId="@xml/default_workspace_6x5"
        launcher:deviceCategory="tablet" >
        <display-option
            launcher:name="Tablet"
            launcher:minWidthDps="900"
            launcher:minHeightDps="820"
            launcher:minCellHeight="120"
            launcher:minCellWidth="102"
            launcher:minCellHeightLandscape="104"
            launcher:minCellWidthLandscape="120"
            launcher:iconImageSize="60"
            launcher:iconTextSize="14"
            launcher:borderSpaceHorizontal="16"
            launcher:borderSpaceVertical="64"
            launcher:borderSpaceLandscapeHorizontal="64"
            launcher:borderSpaceLandscapeVertical="16"
            launcher:horizontalMargin="54"
            launcher:horizontalMarginLandscape="120"
            launcher:allAppsCellWidth="96"
            launcher:allAppsCellHeight="142"
            launcher:allAppsCellWidthLandscape="126"
            launcher:allAppsCellHeightLandscape="126"
            launcher:allAppsIconSize="60"
            launcher:allAppsIconTextSize="14"
            launcher:allAppsBorderSpaceHorizontal="8"
            launcher:allAppsBorderSpaceVertical="16"
            launcher:allAppsBorderSpaceLandscape="16"
            launcher:hotseatBarBottomSpace="76"
            launcher:hotseatBarBottomSpaceLandscape="40"
            launcher:canBeDefault="true" />
    </grid-option>
</profiles>

这里涉及到Hotseat设置地方就下面几个:

  • launcher:numHotseatIcons=“5”:设置显示hotseat的应用数量
  • launcher:hotseatColumnSpanLandscape=“4” : 在横屏模式下,hotseat占用的列数(竖屏模式下,默认占满,即与桌面上应用的列数一致)
  • launcher:hotseatBarBottomSpace=“76”:竖屏模式下的底部间距
  • launcher:hotseatBarBottomSpaceLandscape=“40”:横屏模式下的底部间距

​ 最后添加相应的Hotseat组件,这里有两种情况:

​ (1)如果设备有配置谷歌的GMS包,在Launcher3起来的时候,会加载google_gms包下的这个布局配置文件release\vendor\partner_gms\apps\GmsSampleIntegration\res_dhs_full\xml\partner_default_layout.xml,我的项目中配置了谷歌GMS包,这里给出我的配置:

<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2017 Google Inc. All Rights Reserved. -->
<favorites>
  <!-- Hotseat (We use the screen as the position of the item in the hotseat) -->
  <!-- settings deskclock Calendar Contacts Camera -->
  <favorite container="-101" screen="0" x="0" y="0" packageName="com.android.settings" className="com.android.settings.Settings"/>
  <favorite container="-101" screen="1" x="1" y="0" packageName="com.google.android.deskclock" className="com.android.deskclock.DeskClock"/>
  <favorite container="-101" screen="2" x="2" y="0" packageName="com.google.android.calendar" className="com.android.calendar.event.LaunchInfoActivity"/>
  <favorite container="-101" screen="3" x="3" y="0" packageName="com.google.android.contacts" className="com.android.contacts.activities.PeopleActivity"/>
  <favorite container="-101" screen="4" x="4" y="0" packageName="com.android.camera2" className="com.android.camera.CameraLauncher"/>
  <!-- In Launcher3, workspaces extend infinitely to the right, incrementing from zero -->
  <!-- Google folder -->
  <!-- Google, Chrome, Gmail, Maps, YouTube, (Drive), (Music), (Movies), Duo, Photos -->
  <folder title="@string/google_folder_title" screen="0" x="1" y="3">
    <favorite packageName="com.google.android.googlequicksearchbox" className="com.google.android.googlequicksearchbox.SearchActivity"/>
    <favorite packageName="com.android.chrome" className="com.google.android.apps.chrome.Main"/>
    <favorite packageName="com.google.android.gm" className="com.google.android.gm.ConversationListActivityGmail"/>
    <favorite packageName="com.google.android.apps.maps" className="com.google.android.maps.MapsActivity"/>
    <favorite packageName="com.google.android.youtube" className="com.google.android.youtube.app.honeycomb.Shell$HomeActivity"/>
    <favorite packageName="com.google.android.apps.docs" className="com.google.android.apps.docs.app.NewMainProxyActivity"/>
    <favorite packageName="com.google.android.apps.youtube.music" className="com.google.android.apps.youtube.music.activities.MusicActivity"/>
    <favorite packageName="com.google.android.videos" className="com.google.android.videos.GoogleTvEntryPoint"/>
    <favorite packageName="com.google.android.apps.tachyon" className="com.google.android.apps.tachyon.MainActivity"/>
    <favorite packageName="com.google.android.apps.photos" className="com.google.android.apps.photos.home.HomeActivity"/>
  </folder>
    
  <appwidget screen="0" x="2" y="0" packageName="com.google.android.deskclock" className="com.android.alarmclock.DigitalAppWidgetProvider" spanX="3" spanY="2" />
  <appwidget screen="0" x="1" y="2" packageName="com.google.android.googlequicksearchbox" className="com.google.android.googlequicksearchbox.SearchWidgetProvider" spanX="5" spanY="1" />
</favorites>

效果图如下:

在这里插入图片描述

​ (2)如果设备没有配置谷歌GMS包,那么可以修改Launcher3/res/xml/default_workspace_MxN.xml文件, default_workspace_MxN.xml这是用于定义用户桌面快捷方式(Favorites)配置的一个资源文件,代表主页布局是M行N列,我们可以在里面定义显示在使用这个布局情况下,worksapce中的各个控件。

这里给出修改配置HotSeat的示例:

<?xml version="1.0" encoding="utf-8"?>
<favorites xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3">
	<!--其他配置省略-->
    <resolve>
        <favorite
            launcher:container="-101"
            launcher:className="com.android.settings.Settings"
            launcher:packageName="com.android.settings"
            launcher:screen="0"
            launcher:x="0"
            launcher:y="0" />
    </resolve>
    <resolve>
        <favorite
            launcher:container="-101"
            launcher:className="com.android.deskclock.DeskClock"
            launcher:packageName="com.google.android.deskclock"
            launcher:screen="1"
            launcher:x="1"
            launcher:y="0" />
    </resolve>
    <resolve>
        <favorite
            launcher:container="-101"
            launcher:className="com.android.calendar.event.LaunchInfoActivity"
            launcher:packageName="com.google.android.calendar"
            launcher:screen="2"
            launcher:x="2"
            launcher:y="0" />
    </resolve>
    <resolve>
        <favorite
            launcher:container="-101"
            launcher:className="com.android.contacts.activities.PeopleActivity"
            launcher:packageName="com.google.android.contacts"
            launcher:screen="3"
            launcher:x="3"
            launcher:y="0" />
    </resolve>
    <resolve>
        <favorite
            launcher:container="-101"
            launcher:className="com.android.camera.CameraLauncher"
            launcher:packageName="com.android.camera2"
            launcher:screen="4"
            launcher:x="4"
            launcher:y="0" />
    </resolve>
</favorites>

​ 上面给出示例中配置的Hotseat跟在GMS包的partner_default_layout.xml中配置的效果应该是一样的。因为我这里的项目配置了GMS包,两个配置文件同时存在时,partner_default_layout.xml会覆盖掉default_workspace_MxN.xml的效果,所以在我的项目中是看不到default_workspace_MxN.xml配置效果的,如果有小伙伴按照上面的配置了default_workspace_MxN.xml不起作用,可能需要去参考下其他的配置方式。

Hotseat 图标相关改动

​ Hotseat 布局相关的改动,基本上都可以在Launcher3/src/com/android/launcher3/Hotseat.java中设置。这里贴出源码参考:

// Launcher3/src/com/android/launcher3/Hotseat.java

public class Hotseat extends CellLayout implements Insettable {

    public static final float QSB_CENTER_FACTOR = .325f;
    @ViewDebug.ExportedProperty(category = "launcher")
    private boolean mHasVerticalHotseat;
    private Workspace<?> mWorkspace;
    private boolean mSendTouchToWorkspace;
    @Nullable
    private Consumer<Boolean> mOnVisibilityAggregatedCallback;
    private final View mQsb;

    public Hotseat(Context context) {
        this(context, null);
    }

    public Hotseat(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public Hotseat(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mQsb = LayoutInflater.from(context).inflate(R.layout.search_container_hotseat, this, false);
        addView(mQsb);
    }

    public int getCellXFromOrder(int rank) {
        return mHasVerticalHotseat ? 0 : rank;
    }

    public int getCellYFromOrder(int rank) {
        return mHasVerticalHotseat ? (getCountY() - (rank + 1)) : 0;
    }

    public void resetLayout(boolean hasVerticalHotseat) {
        removeAllViewsInLayout();
        mHasVerticalHotseat = hasVerticalHotseat;
        DeviceProfile dp = mActivity.getDeviceProfile();
        resetCellSize(dp);
        if (hasVerticalHotseat) {
            setGridSize(1, dp.numShownHotseatIcons);
        } else {
            setGridSize(dp.numShownHotseatIcons, 1);
        }
    }

    @Override
    public void setInsets(Rect insets) {
        FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
        DeviceProfile grid = mActivity.getDeviceProfile();

        if (grid.isVerticalBarLayout()) {
            mQsb.setVisibility(View.GONE);
            lp.height = ViewGroup.LayoutParams.MATCH_PARENT;
            if (grid.isSeascape()) {
                lp.gravity = Gravity.LEFT;
                lp.width = grid.hotseatBarSizePx + insets.left;
            } else {
                lp.gravity = Gravity.RIGHT;
                lp.width = grid.hotseatBarSizePx + insets.right;
            }
        } else {
            mQsb.setVisibility(View.VISIBLE);
            lp.gravity = Gravity.BOTTOM;
            lp.width = ViewGroup.LayoutParams.MATCH_PARENT;
            lp.height = grid.hotseatBarSizePx;
        }

        Rect padding = grid.getHotseatLayoutPadding(getContext());
        setPadding(padding.left, padding.top, padding.right, padding.bottom);
        setLayoutParams(lp);
        InsettableFrameLayout.dispatchInsets(this, insets);
    }

    public void setWorkspace(Workspace<?> w) {
        mWorkspace = w;
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int yThreshold = getMeasuredHeight() - getPaddingBottom();
        if (mWorkspace != null && ev.getY() <= yThreshold) {
            mSendTouchToWorkspace = mWorkspace.onInterceptTouchEvent(ev);
            return mSendTouchToWorkspace;
        }
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // See comment in #onInterceptTouchEvent
        if (mSendTouchToWorkspace) {
            final int action = event.getAction();
            switch (action & MotionEvent.ACTION_MASK) {
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL:
                    mSendTouchToWorkspace = false;
            }
            return mWorkspace.onTouchEvent(event);
        }
        return false;
    }

    @Override
    public void onVisibilityAggregated(boolean isVisible) {
        super.onVisibilityAggregated(isVisible);

        if (mOnVisibilityAggregatedCallback != null) {
            mOnVisibilityAggregatedCallback.accept(isVisible);
        }
    }

    public void setOnVisibilityAggregatedCallback(@Nullable Consumer<Boolean> callback) {
        mOnVisibilityAggregatedCallback = callback;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        DeviceProfile dp = mActivity.getDeviceProfile();
        mQsb.measure(MeasureSpec.makeMeasureSpec(dp.hotseatQsbWidth, MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(dp.hotseatQsbHeight, MeasureSpec.EXACTLY));
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);

        int qsbMeasuredWidth = mQsb.getMeasuredWidth();
        int left;
        DeviceProfile dp = mActivity.getDeviceProfile();
        if (dp.isQsbInline) {
            int qsbSpace = dp.hotseatBorderSpace;
            left = Utilities.isRtl(getResources()) ? r - getPaddingRight() + qsbSpace
                    : l + getPaddingLeft() - qsbMeasuredWidth - qsbSpace;
        } else {
            left = (r - l - qsbMeasuredWidth) / 2;
        }
        int right = left + qsbMeasuredWidth;

        int bottom = b - t - dp.getQsbOffsetY();
        int top = bottom - dp.hotseatQsbHeight;
        mQsb.layout(left, top, right, bottom);
    }

    public void setIconsAlpha(float alpha) {
        getShortcutsAndWidgets().setAlpha(alpha);
    }

    public void setQsbAlpha(float alpha) {
        mQsb.setAlpha(alpha);
    }

    public float getIconsAlpha() {
        return getShortcutsAndWidgets().getAlpha();
    }

    public View getQsb() {
        return mQsb;
    }
}

由上面源码可以看出,Hotseat也是继承于CellLayout,所以Hotseat中布局相关的修改也是和workspace中的普通应用是类似的。这里给出几个关键方法的说明

(1)确认Hotseat使用横向布局还是纵向布局:

  • 纵向布局,Hotseat会出现在屏幕的侧边;
  • 横向布局,Hotseat会出现在屏幕的底部;
// Launcher3/src/com/android/launcher3/Hotseat.java

public void resetLayout(boolean hasVerticalHotseat) {
    removeAllViewsInLayout();
    mHasVerticalHotseat = hasVerticalHotseat;
    DeviceProfile dp = mActivity.getDeviceProfile();
    resetCellSize(dp);
    if (hasVerticalHotseat) {
        // 竖屏布局,Hotseat SpanX=1,SpanY=numShownHotseatIcons
        setGridSize(1, dp.numShownHotseatIcons);
    } else {
        // 横屏布局刚好相反,SpanX=numShownHotseatIcons,SpanY=1
        setGridSize(dp.numShownHotseatIcons, 1);
    }
}

这个方法是在Launcher.java的startBinding()中调用的:

// Launcher3/src/com/android/launcher3/Launcher.java
public void startBinding() {

    mWorkspace.clearDropTargets();
    mWorkspace.removeAllWorkspaceScreens();
    mAppWidgetHolder.clearViews();

    // 初始化Hotseat的横竖屏布局
    if (mHotseat != null) {
        // isVerticalBarLayout()判断的逻辑是 :
        // 1.屏幕的宽度大于等于最小的TABLET宽度
        // 2.屏幕宽度是否大于高度
        mHotseat.resetLayout(getDeviceProfile().isVerticalBarLayout());
    }
    TraceHelper.INSTANCE.endSection(traceToken);
}

(2)设置图标padding

// Launcher3/src/com/android/launcher3/Hotseat.java
@Override
public void setInsets(Rect insets) {
    FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
    DeviceProfile grid = mActivity.getDeviceProfile();

    if (grid.isVerticalBarLayout()) {
        // 左右两侧显示的,高度铺满,宽度限定,根据seascape决定显示在左边还是右边
        mQsb.setVisibility(View.GONE);
        lp.height = ViewGroup.LayoutParams.MATCH_PARENT;
        if (grid.isSeascape()) {
            lp.gravity = Gravity.LEFT;
            lp.width = grid.hotseatBarSizePx + insets.left;
        } else {
            lp.gravity = Gravity.RIGHT;
            lp.width = grid.hotseatBarSizePx + insets.right;
        }
    } else {
        // 底部显示,宽度铺满,高度限定
        mQsb.setVisibility(View.VISIBLE);
        lp.gravity = Gravity.BOTTOM;
        lp.width = ViewGroup.LayoutParams.MATCH_PARENT;
        lp.height = grid.hotseatBarSizePx;
    }

    // 计算padding,设置了padding就相当于决定了child显示的位置
    Rect padding = grid.getHotseatLayoutPadding(getContext());
    setPadding(padding.left, padding.top, padding.right, padding.bottom);
    setLayoutParams(lp);
    InsettableFrameLayout.dispatchInsets(this, insets);
}

关闭Hotseat应用预测功能

​ 在Android13中,Hotseat提供了预测应用的功能:当你开启了hotseat功能,并且hotseat上面有空白位置时(没有预先设定hotseat应用或者将hotseat上的应用拖走时),系统会根据 应用使用历史、最近使用的应用、安装的新应用等数据,预测用户可能想要打开的应用程序,然后在Hotseat的空白位置上显示这些预测的应用程序图标。

使用应用预测的效果如下:

在这里插入图片描述

拖动第四个Hotseat到桌面,下图是松手后的效果:

在这里插入图片描述

​ 上面示例是拖动第四个Hotseat应用到桌面上,松手之后发现原本第四个hotseat应用的位置出现了一个谷歌浏览器的图标,这个自动补全的过程是系统完成的,这就是Hotseat预测应用的功能。移动其他位置的Hotseat应用也会有同样的效果,补全的应用由系统确定。
​ 通过上面效果图也能发现,如果是预测的应用,应用图标周边会有一圈浅黄色(也可能是其他颜色),这可以用来区分当前的Hotseat是自己设定的还是预测的应用。
​ 另外,预测的效果是一直存在的,拖走一个马上又会生成一个新的。也就是说,你可以无限制地拖动Hotseat上的应用到桌面上。

​ 对某些用户来说,不太需要应用预测这个功能,但是系统中没有关闭Hotseat应用预测功能的入口,所以需要在代码中修改相关的逻辑。

​ hotseat应用预测相关的类是:Launcher3/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java;修改下面代码即可在主页中关闭Hotseat应用预测功能:

// Launcher3/quickstep/src/com/android/launcher3/hybridhotseat/HotseatPredictionController.java

private int mHotSeatItemsCount;

public HotseatPredictionController(QuickstepLauncher launcher) {
    mLauncher = launcher;
    mHotseat = launcher.getHotseat();
    mHotSeatItemsCount = mLauncher.getDeviceProfile().numShownHotseatIcons;
//        mHotSeatItemsCount = 0;
    mLauncher.getDragController().addDragListener(this);

    launcher.addOnDeviceProfileChangeListener(this);
    mHotseat.getShortcutsAndWidgets().setOnHierarchyChangeListener(this);
}

@Override
public void onDeviceProfileChanged(DeviceProfile profile) {
    this.mHotSeatItemsCount = profile.numShownHotseatIcons;
//        this.mHotSeatItemsCount = 0;
}

修改思路:在HotseatPredictionController.java中,mHotSeatItemsCount这个属性记录了预测应用的个数,我们在初始化的时候设定为0即可。在移走Hotseat应用时,就不会自动补全预测应用了,效果如下:

在这里插入图片描述

可以看到,将第四个应用拖拽到桌面后,第四个Hotseat的位置没有出现预测的应用了,说明上面的改动是ok的。

禁止拖动Hotseat中的应用

Hotseat和普通桌面应用一样,都是可以拖拽进行移动、合并成文件夹的;有些用户可能想完全固定住Hotseat应用,即不允许拖拽,也不允许合并成文件夹,可以按照下面的修改来实现:

修改Workspace.java中拖拽的相关逻辑代码:

// Launcher3/src/com/android/launcher3/Workspace.java

// 在处理拖拽图标的方法中对Hotseat应用做过滤
@Override
public void onDrop(final DragObject d, DragOptions options) {
    mDragViewVisualCenter = d.getVisualCenter(mDragViewVisualCenter);
    CellLayout dropTargetLayout = mDropToLayout;

    // 省略其他代码...
    Runnable onCompleteRunnable = null;
    if (d.dragSource != this || mDragInfo == null) {
        final int[] touchXY = new int[] { (int) mDragViewVisualCenter[0],
                (int) mDragViewVisualCenter[1] };
        onDropExternal(touchXY, dropTargetLayout, d);
    } else {
        final View cell = mDragInfo.cell;
        boolean droppedOnOriginalCellDuringTransition = false;
        // 这里增加判断逻辑,如果是Hotseat类的应用,就不进行拖拽
        // if (dropTargetLayout != null && !d.cancelled) {
        int drag_container = mDragInfo.container;
        if (dropTargetLayout != null && !d.cancelled && !mLauncher.isHotseatLayout(dropTargetLayout) && drag_container!=-101) {
            // Move internally

            // 省略其他代码...
        }
        // 省略其他代码...
    }
}

​ 修改之后,拖拽Hotseat中的应用到桌面中会失败,hotseat应用会自动回到原来的Hotseat中,同样的,也不能从桌面中拖拽普通应用到Hotseat中,Hotseat之间也不能拖拽合并生成文件夹。

禁用Hotseat

​ 有些用户想要禁用Hotseat应用功能,但是Launcher3没有提供相应的设置入口,只能通过改动源码来实现。这里提供两种方式,其中第二种方式可能有点缺陷,大家可以参考使用:

(1)方法一:参考资料:【安卓13】谷歌原生桌面launcher3源码修改,修改桌面布局(首屏应用、小部件、导航栏、大屏设备任务栏)_安卓原生桌面-CSDN博客

修改逻辑:将Launcher3/res/xml/device_profiles.xml中配置的numHotseatIcons属性值改为0,然后在使用到该属性的地方做处理(主要是在DeviceProfile.java中)。因为在源码中有多处地方在计算布局的时候,都会获取numHotseatIcons的值来做计算,包括除法运算,所以numHotseatIcons的值设为0或者1之后,会引发 java.lang.ArithmeticException: divide by zero等报错。

Launcher3/res/xml/device_profiles.xml的配置:

<!--Launcher3/res/xml/device_profiles.xml-->
<?xml version="1.0" encoding="utf-8"?>
<profiles xmlns:launcher="http://schemas.android.com/apk/res-auto" >
    <其他的grid-option布局...>
        
    <grid-option
        launcher:name="6_by_5"
        launcher:numRows="6"
        launcher:numColumns="7"
        launcher:numSearchContainerColumns="5"
        launcher:numFolderRows="3"
        launcher:numFolderColumns="4"
        launcher:numHotseatIcons="0"
        launcher:hotseatColumnSpanLandscape="4"
        launcher:numAllAppsColumns="6"
        launcher:isScalable="true"
        launcher:inlineNavButtonsEndSpacing="@dimen/taskbar_button_margin_6_5"
        launcher:devicePaddingId="@xml/paddings_6x5"
        launcher:dbFile="launcher_6_by_5.db"
        launcher:defaultLayoutId="@xml/default_workspace_6x5"
        launcher:deviceCategory="tablet" >
        <display-option
            launcher:name="Tablet"
            .../>
    </grid-option>
</profiles>

关键点:修改launcher:numHotseatIcons=“0”

然后在Launcher3/src/com/android/launcher3/DeviceProfile.java中进行修改:

// Launcher3/src/com/android/launcher3/DeviceProfile.java

public void recalculateHotseatWidthAndBorderSpace() {
    if (!isScalableGrid) return;
    // ...
    maxHotseatIconsWidth = maxHotseatWidth - (isQsbInline ? hotseatQsbWidth : 0);
    do {
        // numShownHotseatIcons--;
        // 1.增加一个判断,当numShownHotseatIcons > 0时才进行--操作
        if(numShownHotseatIcons > 0){
            numShownHotseatIcons--;
        }
        hotseatBorderSpace = calculateHotseatBorderSpace(maxHotseatIconsWidth,
                (isQsbInline ? 1 : 0) + 1);
    } while (hotseatBorderSpace < minHotseatIconSpacePx && numShownHotseatIcons > 1);
}

public Rect getHotseatLayoutPadding(Context context) {
    Rect hotseatBarPadding = new Rect();
    if (isVerticalBarLayout()) {
        // ...
    } else {
        float workspaceCellWidth = (float) widthPx / inv.numColumns;
        // float hotseatCellWidth = (float) widthPx / numShownHotseatIcons;
        // 2.numShownHotseatIcons为0时不能直接作为除数
        float hotseatCellWidth = (float) widthPx / numShownHotseatIcons== 0 ? 3 : numShownHotseatIcons;
        int hotseatAdjustment = Math.round((workspaceCellWidth - hotseatCellWidth) / 2);
		// ...
    }
    return hotseatBarPadding;
}

private int calculateHotseatBorderSpace(float hotseatWidthPx, int numExtraBorder) {
    // 3.注释numShownHotseatIcons相关计算,直接返回iconSizePx
//        float hotseatIconsTotalPx = iconSizePx * numShownHotseatIcons;
//        int hotseatBorderSpace =
//                (int) (hotseatWidthPx - hotseatIconsTotalPx)
//                        / (numShownHotseatIcons - 1 + numExtraBorder);
//        return Math.min(hotseatBorderSpace, maxHotseatIconSpacePx);
    return iconSizePx;
}

private int getVerticalHotseatLastItemBottomOffset(Context context) {
    Rect hotseatBarPadding = getHotseatLayoutPadding(context);
    // 4.calculateCellHeight方法使用了numShownHotseatIcons,需要在calculateCellWidth方法内部做处理
    int cellHeight = calculateCellHeight(
            heightPx - hotseatBarPadding.top - hotseatBarPadding.bottom, hotseatBorderSpace, numShownHotseatIcons);
    int extraIconEndSpacing = (cellHeight - iconSizePx) / 2;
    return extraIconEndSpacing + hotseatBarPadding.bottom;
}

public static int calculateCellWidth(int width, int borderSpacing, int countX) {
    // 4.1
    //return (width - ((countX - 1) * borderSpacing)) / countX;
    if(countX > 1){
        return (width - ((countX - 1) * borderSpacing)) / countX;
    }else{
        return (width - borderSpacing) / 2;
    }
}

public static int calculateCellHeight(int height, int borderSpacing, int countY) {
    // 4.2
    //return (height - ((countY - 1) * borderSpacing)) / countY;
    if (countY > 1){
        return (height - ((countY - 1) * borderSpacing)) / countY;
    }else{
        return (height - (borderSpacing)) / 2;
    }
}

上面总共有四个点需要修改,都写在注释里了。修改后的效果如下:

在这里插入图片描述

点开最近应用页面:

在这里插入图片描述

由上面的效果可以看出,桌面已经移除了Hotseat的应用,并且也无法成功拖拽普通的workspace应用到原先Hotseat的位置了,最后,在打开最近应用页面中也看到不到Hotseat的应用了,说明上面禁用Hotseat的代码是改动成功了。

(2)方法二:分享另外一个博主的方法(因代码不同,我这里在他的基础上增加了一些改动);博客原文:Android10/11 原生Launcher3深度定制开发_launcher3 九宫格-CSDN博客

先在Launcher.xml中修改Hotseat组件不可见:

<include
    android:id="@+id/hotseat"
    layout="@layout/hotseat"
    android:visibility="gone"/>

在Hotseat.java实现类的构造方法、绘制方法、刷新方法中,设置布局不可见,同时将触摸监听和拦截的逻辑注释掉:

// Launcher3/src/com/android/launcher3/Hotseat.java

public class Hotseat extends CellLayout implements Insettable {
    // ...其他代码省略

    // 构造方法中设置视图为GONE
    public Hotseat(Context context) {
        this(context, null);
        this.setVisibility(View.GONE);
    }

    public Hotseat(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
        this.setVisibility(View.GONE);
    }

    public Hotseat(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        mQsb = LayoutInflater.from(context).inflate(R.layout.search_container_hotseat, this, false);
        addView(mQsb);
        this.setVisibility(View.GONE);
    }

    public void resetLayout(boolean hasVerticalHotseat) {
        // ...
        if (hasVerticalHotseat) {
            setGridSize(1, dp.numShownHotseatIcons);
        } else {
            setGridSize(dp.numShownHotseatIcons, 1);
        }
        // 设置视图为GONE
        this.setVisibility(View.GONE);
    }

    @Override
    public void setInsets(Rect insets) {
        // ...
        setLayoutParams(lp);
        InsettableFrameLayout.dispatchInsets(this, insets);
        // 设置视图为GONE
        this.setVisibility(View.GONE);
    }

    // 将触摸监听和拦截的逻辑注释掉
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        // We allow horizontal workspace scrolling from within the Hotseat. We do this by delegating
        // touch intercept the Workspace, and if it intercepts, delegating touch to the Workspace
        // for the remainder of the this input stream.
//        int yThreshold = getMeasuredHeight() - getPaddingBottom();
//        if (mWorkspace != null && ev.getY() <= yThreshold) {
//            mSendTouchToWorkspace = mWorkspace.onInterceptTouchEvent(ev);
//            return mSendTouchToWorkspace;
//        }
        return false;
    }

    // 将触摸监听和拦截的逻辑注释掉
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // See comment in #onInterceptTouchEvent
//        if (mSendTouchToWorkspace) {
//            final int action = event.getAction();
//            switch (action & MotionEvent.ACTION_MASK) {
//                case MotionEvent.ACTION_UP:
//                case MotionEvent.ACTION_CANCEL:
//                    mSendTouchToWorkspace = false;
//            }
//            return mWorkspace.onTouchEvent(event);
//        }
        // Always let touch follow through to Workspace.
        return false;
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);

        int qsbMeasuredWidth = mQsb.getMeasuredWidth();
        int left;
        DeviceProfile dp = mActivity.getDeviceProfile();
        if (dp.isQsbInline) {
            int qsbSpace = dp.hotseatBorderSpace;
            left = Utilities.isRtl(getResources()) ? r - getPaddingRight() + qsbSpace
                    : l + getPaddingLeft() - qsbMeasuredWidth - qsbSpace;
        } else {
            left = (r - l - qsbMeasuredWidth) / 2;
        }
        int right = left + qsbMeasuredWidth;

        int bottom = b - t - dp.getQsbOffsetY();
        int top = bottom - dp.hotseatQsbHeight;
        mQsb.layout(left, top, right, bottom);
        // 设置视图为GONE
        this.setVisibility(View.GONE);
    }
}

在Launcher.java初始化控件的setupViews方法中GONE布局:

// Launcher3/src/com/android/launcher3/Launcher.java

    protected void setupViews() {
        inflateRootView(R.layout.launcher);
        mDragLayer = findViewById(R.id.drag_layer);
        mFocusHandler = mDragLayer.getFocusIndicatorHelper();
        mWorkspace = mDragLayer.findViewById(R.id.workspace);
        mWorkspace.initParentViews(mDragLayer);
        mOverviewPanel = findViewById(R.id.overview_panel);
        mHotseat = findViewById(R.id.hotseat);
        mHotseat.setWorkspace(mWorkspace);

        // 省略代码...
        mDropTargetBar.setup(mDragController);
        mAllAppsController.setupViews(mScrimView, mAppsView);

        // hide Hotseat++
        mHotseat.setVisibility(View.GONE);
    }

在LauncherPreviewRenderer.java 的MainThreadRenderer内部类中GONE布局:

// Launcher3/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java

public LauncherPreviewRenderer(Context context,
        InvariantDeviceProfile idp,
        WallpaperColors wallpaperColorsOverride,
        @Nullable final SparseArray<Size> launcherWidgetSpanInfo) {

    // ...
   int layoutRes = mDp.isTwoPanels ? R.layout.launcher_preview_two_panel_layout
            : R.layout.launcher_preview_layout;
    mRootView = (InsettableFrameLayout) mHomeElementInflater.inflate(
            layoutRes, null, false);
    mRootView.setInsets(mInsets);
    measureView(mRootView, mDp.widthPx, mDp.heightPx);

    mHotseat = mRootView.findViewById(R.id.hotseat);
    mHotseat.resetLayout(false);

    // hide Hotseat
    mHotseat.setVisibility(View.GONE);

    // 省略代码...
}

最终效果如下:

在这里插入图片描述

但是这种方式有个bug,桌面上确实是去掉了Hotseat,但是打开最近应用页面时,还是能看到Hotseat:
在这里插入图片描述
这个后面需要再排查下在哪里修改这部分的逻辑。

设置导航栏居中

​ 在大屏的布局中,导航栏默认是在右下角的,项目的主页隐藏了Hotseat之后,需要将导航栏设置到居中位置的。

​ 安卓系统会对设备做一个判断,如果设备是大屏(即设置了launcher:deviceCategory=“tablet”)才会设置导航栏在右侧,但是由于开发需要,我们现在要保留tablet这个属性,所以就不能通过改变设备为手机模式来控制导航栏居中。【如果是手机等竖屏设备,可以修改isTablet这个属性为false来设定导航栏居中】

​ 在Launcher3\res\xml\device_profiles.xml布局文件中,有个属性launcher:inlineNavButtonsEndSpacing 定义了内联导航按钮的结束间距,也就是说,这个变量控制着底部导航栏距离右边框的dp值,数值越大,越往中间靠拢。所以可以通过修改launcher:inlineNavButtonsEndSpacing来控制导航栏位置:

launcher:inlineNavButtonsEndSpacing="@dimen/taskbar_button_margin_split"
// Launcher3/src/com/android/launcher3/DeviceProfile.java

DeviceProfile(Context context, InvariantDeviceProfile inv, Info info, WindowBounds windowBounds,
            SparseArray<DotRenderer> dotRendererCache, boolean isMultiWindowMode,
            boolean transposeLayoutWithOrientation, boolean isMultiDisplay, boolean isGestureMode,
            @NonNull final ViewScaleProvider viewScaleProvider) {
    
    // 代码省略...
    
    // 计算导航栏离右边的距离
    if (areNavButtonsInline && !isPhone) {
        inlineNavButtonsEndSpacing = res.getDimensionPixelSize(inv.inlineNavButtonsEndSpacing);
        /*
         * 3 nav buttons +
         * Spacing between nav buttons +
         * Space at the end for contextual buttons
         */
        hotseatBarEndOffset = 3 * res.getDimensionPixelSize(R.dimen.taskbar_nav_buttons_size)
                + 2 * res.getDimensionPixelSize(R.dimen.taskbar_button_space_inbetween)
                + inlineNavButtonsEndSpacing;
    } else {
        inlineNavButtonsEndSpacing = 0;
        hotseatBarEndOffset = 0;
    }
	// ...   
}

这里影响导航栏位置的有三个值:taskbar_nav_buttons_size、taskbar_button_space_inbetween和inv.inlineNavButtonsEndSpacing,这里我们直接修改inv.inlineNavButtonsEndSpacing对应的属性值即可:

<!--Launcher3/quickstep/res/values-land/dimens.xml-->
<!--根据大屏宽度设定合适的值-->
<dimen name="taskbar_button_margin_split">550dp</dimen>

修改前后的效果图如下:

修改前:

在这里插入图片描述

修改后:

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值