基于Android7.0的Launcher3源码分析(3)——显示相关参数的初始化

20 阅读8分钟

  前面第一篇讲解了Launcher从初始化到显示的大概流程。接下来的文章将会一步步详细分析这些流程。今天这篇文章讲下Launcher显示相关参数的初始化和处理过程。 初始化入口

  从onCreate()进入,首先判断了Launcher的排列方向,一般都是纵向的,所以本文就以纵向为例。通过app.getInvariantDeviceProfile().portraitProfile 获取到mDeviceProfile的实例对象,该对象中以成员变量的形式存储了显示相关参数。然后,根据这些参数,通过 mDeviceProfile.layout() 对Launcher的控件进行布局的调整。Launcher主要有哪些控件组成,详见文章 Launcher3源码分析(2)——UI显示控件的组成   下面围绕两个方面展开。1、mDeviceProfile 对象如何初始化;2、如何根据 mDeviceProfile 对象中保存的数据调整布局参数。

public class Launcher extends Activity implements View.OnClickListener, OnLongClickListener, LauncherModel.Callbacks, View.OnTouchListener, PageSwitchListener, LauncherProviderChangeListener { ......

private DeviceProfile mDeviceProfile;
@Override
protected void onCreate(Bundle savedInstanceState) {
    ......
    LauncherAppState app = LauncherAppState.getInstance();

    // Load configuration-specific DeviceProfile
    mDeviceProfile = getResources().getConfiguration().orientation
            == Configuration.ORIENTATION_LANDSCAPE ?
            app.getInvariantDeviceProfile().landscapeProfile
            : app.getInvariantDeviceProfile().portraitProfile;
    ......
    setContentView(R.layout.launcher);
    setupViews();

    mDeviceProfile.layout(this);

    ......
}

}

1、mDeviceProfile 对象初始化 1.1 LauncherAppState初始化

  可以看到,这里获取了LauncherAppState的一个单例对象,看下它的初始化。这里最重要的是成员对象 mInvariantDeviceProfile 的初始化,因为 app.getInvariantDeviceProfile() 就是获取的该对象。

public class LauncherAppState {

......

private static LauncherAppState INSTANCE;
private InvariantDeviceProfile mInvariantDeviceProfile;

public static LauncherAppState getInstance() {
    if (INSTANCE == null) {
        INSTANCE = new LauncherAppState();
    }
    return INSTANCE;
}

......

private LauncherAppState() {
    ......

    mInvariantDeviceProfile = new InvariantDeviceProfile(sContext);
    mIconCache = new IconCache(sContext, mInvariantDeviceProfile);
    mWidgetCache = new WidgetPreviewLoader(sContext, mIconCache);

    mAppFilter = AppFilter.loadByName(sContext.getString(R.string.app_filter_class));
    mModel = new LauncherModel(this, mIconCache, mAppFilter);

    ......
}

public InvariantDeviceProfile getInvariantDeviceProfile() {
    return mInvariantDeviceProfile;
}
......

}

1.2 InvariantDeviceProfile初始化

  来到InvariantDeviceProfile的单个参数的构造方法。可以看到,之前在Launcher中app.getInvariantDeviceProfile().portraitProfile 的 portraitProfile 就是在这里初始化的。下面一步步来看。

public class InvariantDeviceProfile { ......

DeviceProfile portraitProfile;

@TargetApi(23)
InvariantDeviceProfile(Context context) {
    WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    Display display = wm.getDefaultDisplay();
    DisplayMetrics dm = new DisplayMetrics();
    display.getMetrics(dm);

    Point smallestSize = new Point();
    Point largestSize = new Point();
    display.getCurrentSizeRange(smallestSize, largestSize); //获取屏幕的最大最小尺寸

    // This guarantees that width < height
    minWidthDps = Utilities.dpiFromPx(Math.min(smallestSize.x, smallestSize.y), dm); //px转dp
    minHeightDps = Utilities.dpiFromPx(Math.min(largestSize.x, largestSize.y), dm); //px转dp

    ArrayList<InvariantDeviceProfile> closestProfiles =
            findClosestDeviceProfiles(minWidthDps, minHeightDps, getPredefinedDeviceProfiles());
    InvariantDeviceProfile interpolatedDeviceProfileOut =
            invDistWeightedInterpolate(minWidthDps,  minHeightDps, closestProfiles);

    InvariantDeviceProfile closestProfile = closestProfiles.get(0);
    numRows = closestProfile.numRows;
    numColumns = closestProfile.numColumns;
    numHotseatIcons = closestProfile.numHotseatIcons;
    hotseatAllAppsRank = (int) (numHotseatIcons / 2);
    defaultLayoutId = closestProfile.defaultLayoutId;
    numFolderRows = closestProfile.numFolderRows;
    numFolderColumns = closestProfile.numFolderColumns;
    minAllAppsPredictionColumns = closestProfile.minAllAppsPredictionColumns;

    iconSize = interpolatedDeviceProfileOut.iconSize; //初始图标大小
    iconBitmapSize = Utilities.pxFromDp(iconSize, dm);
    iconTextSize = interpolatedDeviceProfileOut.iconTextSize; //初始文字大小
    hotseatIconSize = interpolatedDeviceProfileOut.hotseatIconSize;
    fillResIconDpi = getLauncherIconDensity(iconBitmapSize);

    // If the partner customization apk contains any grid overrides, apply them
    // Supported overrides: numRows, numColumns, iconSize
    applyPartnerDeviceProfileOverrides(context, dm);

    Point realSize = new Point();
    display.getRealSize(realSize); //获取屏幕实际的尺寸
    // The real size never changes. smallSide and largeSide will remain the
    // same in any orientation.
    int smallSide = Math.min(realSize.x, realSize.y);
    int largeSide = Math.max(realSize.x, realSize.y);

    landscapeProfile = new DeviceProfile(context, this, smallestSize, largestSize,
            largeSide, smallSide, true /* isLandscape */);
    portraitProfile = new DeviceProfile(context, this, smallestSize, largestSize,
            smallSide, largeSide, false /* isLandscape */);
}

1.2.1 获取屏幕的最大和最小尺寸

  通过getCurrentSizeRange方法返回应用程序可以操作的最大和最小尺寸范围,赋值给largestSize和smallestSize。     largestSize.x:屏幕最大可用宽度; largestSize.y:屏幕最大可用高度     smallestSize.x:屏幕最小可用宽度; smallestSize.y:屏幕最小可用高度   什么是最大最小尺寸呢?我们知道,Android系统界面上会显示系统控件,如statusBar和navigationBar,当这些控件都显示时,可用的最小屏幕高度将会比屏幕的实际高度小,也就是可用最小范围。当statusBar这些系统控件都被隐藏时,就可能返回屏幕可用高度的最大值。   上面说的largestSize.x等都是以px为单位的,为了更好的适配分辨率,需要将其转化为dp为单位。通过dpiFromPx方法将其进行转化,然后把屏幕可用宽度的dp值赋值给minWidthDps,将屏幕可用高度的dp值赋值给minHeightDps。下面会根据这两个值来确定一部分参数的值。 1.2.2 根据设备配置参数获得当前图标和文字大小

  可以看到,这里针对一些手机机型,进行了一些适配,定义了图标大小,默认的桌面行列布局等参数。并以ArrayList的形式进行存储,并返回给findClosestDeviceProfiles方法当做传入的参数。

ArrayList<InvariantDeviceProfile> getPredefinedDeviceProfiles() {
    ArrayList<InvariantDeviceProfile> predefinedDeviceProfiles = new ArrayList<>();
    // width, height, #rows, #columns, #folder rows, #folder columns,
    // iconSize, iconTextSize, #hotseat, #hotseatIconSize, defaultLayoutId.
    predefinedDeviceProfiles.add(new InvariantDeviceProfile("Super Short Stubby",
            255, 300,     2, 3, 2, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_3x3));
    predefinedDeviceProfiles.add(new InvariantDeviceProfile("Shorter Stubby",
            255, 400,     3, 3, 3, 3, 3, 48, 13, 3, 48, R.xml.default_workspace_3x3));
    predefinedDeviceProfiles.add(new InvariantDeviceProfile("Short Stubby",
            275, 420,     3, 4, 3, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4));
    predefinedDeviceProfiles.add(new InvariantDeviceProfile("Stubby",
            255, 450,     3, 4, 3, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4));
    predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus S",
            296, 491.33f, 4, 4, 4, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4));
    predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 4",
            359, 567,     4, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4));
    predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 5",
            335, 567,     4, 4, 4, 4, 4, DEFAULT_ICON_SIZE_DP, 13, 5, 56, R.xml.default_workspace_4x4));
    predefinedDeviceProfiles.add(new InvariantDeviceProfile("Large Phone",
            406, 694,     5, 5, 4, 4, 4, 64, 14.4f,  5, 56, R.xml.default_workspace_5x5));
    // The tablet profile is odd in that the landscape orientation
    // also includes the nav bar on the side
    predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 7",
            575, 904,     5, 6, 4, 5, 4, 72, 14.4f,  7, 60, R.xml.default_workspace_5x6));
    // Larger tablet profiles always have system bars on the top & bottom
    predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus 10",
            727, 1207,    5, 6, 4, 5, 4, 76, 14.4f,  7, 76, R.xml.default_workspace_5x6));
    predefinedDeviceProfiles.add(new InvariantDeviceProfile("20-inch Tablet",
            1527, 2527,   7, 7, 6, 6, 4, 100, 20,  7, 72, R.xml.default_workspace_5x6));
    return predefinedDeviceProfiles;
}

  来到findClosestDeviceProfiles方法。该方法根据之前获取的屏幕可用宽高,来把getPredefinedDeviceProfiles获取到的不同机型的参数进行排序。这里采取一个升序的形式排序,即与屏幕可用宽高相差越小的机型配置排在越前面。这样返回的pointsByNearness链表中,排在第一个的就是与当前屏幕可用宽高最为接近的一组配置。

ArrayList<InvariantDeviceProfile> findClosestDeviceProfiles(
        final float width, final float height, ArrayList<InvariantDeviceProfile> points) {

    // Sort the profiles by their closeness to the dimensions
    ArrayList<InvariantDeviceProfile> pointsByNearness = points;
    Collections.sort(pointsByNearness, new Comparator<InvariantDeviceProfile>() {
        public int compare(InvariantDeviceProfile a, InvariantDeviceProfile b) {
            return Float.compare(dist(width, height, a.minWidthDps, a.minHeightDps),
                    dist(width, height, b.minWidthDps, b.minHeightDps));
        }
    });

    return pointsByNearness;
}

  来到invDistWeightedInterpolate方法。拿到排在第一组的机型参数,如果与当前屏幕可用宽高完全吻合,即直接返回该组机型的参数。如果没有完全吻合,则经过一系列的计算,来得到与当前屏幕可用宽高最为合适的各参数大小。具体算法这边就不细究了。

InvariantDeviceProfile invDistWeightedInterpolate(float width, float height,
            ArrayList<InvariantDeviceProfile> points) {
    float weights = 0;

    InvariantDeviceProfile p = points.get(0);
    if (dist(width, height, p.minWidthDps, p.minHeightDps) == 0) {
        return p;
    }

    InvariantDeviceProfile out = new InvariantDeviceProfile();
    for (int i = 0; i < points.size() && i < KNEARESTNEIGHBOR; ++i) {
        p = new InvariantDeviceProfile(points.get(i));
        float w = weight(width, height, p.minWidthDps, p.minHeightDps, WEIGHT_POWER);
        weights += w;
        out.add(p.multiply(w));
    }
    return out.multiply(1.0f/weights);
}

  计算完毕后,对重要参数进行赋值,留待使用。这里的iconSize等参数只是一个基础大小,并不是最终大小,后面可能根据需要还会有一些处理。

    iconSize = interpolatedDeviceProfileOut.iconSize;
    iconBitmapSize = Utilities.pxFromDp(iconSize, dm);
    iconTextSize = interpolatedDeviceProfileOut.iconTextSize;
    hotseatIconSize = interpolatedDeviceProfileOut.hotseatIconSize;

1.2.3 获取屏幕的实际尺寸

  通过getRealSize返回屏幕可用大小的实际尺寸。

  为了方便理解,这里以屏幕物理大小为 720*1280 的手机为例,得到的输出参数如下:   - 最小可用尺寸:smallestSize.x = 720 smallestSize.y = 670   - 最大可用尺寸:largestSize.x = 1196 largestSize.y = 1134   - 实际尺寸:realSize.x = 720 realSize.y = 1280 1.2.4 完成实例化

  将InvariantDeviceProfile对象, smallestSize, largestSize, smallSide, largeSide作为参数传入,就到了DeviceProfile对象的实例化的最后一步。

public DeviceProfile(Context context, InvariantDeviceProfile inv,
        Point minSize, Point maxSize,
        int width, int height, boolean isLandscape) {

    this.inv = inv;
    this.isLandscape = isLandscape;

    Resources res = context.getResources();
    DisplayMetrics dm = res.getDisplayMetrics();

    // Constants from resources
    isTablet = res.getBoolean(R.bool.is_tablet);
    isLargeTablet = res.getBoolean(R.bool.is_large_tablet);
    isPhone = !isTablet && !isLargeTablet;

    // Some more constants
    transposeLayoutWithOrientation =
            res.getBoolean(R.bool.hotseat_transpose_layout_with_orientation);

    ComponentName cn = new ComponentName(context.getPackageName(),
            this.getClass().getName());
    defaultWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(context, cn, null);
    edgeMarginPx = res.getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin);
    desiredWorkspaceLeftRightMarginPx = 2 * edgeMarginPx;
    pageIndicatorHeightPx =
            res.getDimensionPixelSize(R.dimen.dynamic_grid_page_indicator_height);
    defaultPageSpacingPx =
            res.getDimensionPixelSize(R.dimen.dynamic_grid_workspace_page_spacing);
    overviewModeMinIconZoneHeightPx =
            res.getDimensionPixelSize(R.dimen.dynamic_grid_overview_min_icon_zone_height);
    overviewModeMaxIconZoneHeightPx =
            res.getDimensionPixelSize(R.dimen.dynamic_grid_overview_max_icon_zone_height);
    overviewModeBarItemWidthPx =
            res.getDimensionPixelSize(R.dimen.dynamic_grid_overview_bar_item_width);
    overviewModeBarSpacerWidthPx =
            res.getDimensionPixelSize(R.dimen.dynamic_grid_overview_bar_spacer_width);
    overviewModeIconZoneRatio =
            res.getInteger(R.integer.config_dynamic_grid_overview_icon_zone_percentage) / 100f;
    iconDrawablePaddingOriginalPx =
            res.getDimensionPixelSize(R.dimen.dynamic_grid_icon_drawable_padding);

    // AllApps uses the original non-scaled icon text size
    allAppsIconTextSizeSp = inv.iconTextSize;

    // AllApps uses the original non-scaled icon size
    allAppsIconSizePx = Utilities.pxFromDp(inv.iconSize, dm);

    // Determine sizes.
    widthPx = width;
    heightPx = height;
    if (isLandscape) {
        availableWidthPx = maxSize.x;
        availableHeightPx = minSize.y;
    } else {
        availableWidthPx = minSize.x;
        availableHeightPx = maxSize.y;
    }

    // Calculate the remaining vars
    updateAvailableDimensions(dm, res);
    computeAllAppsButtonSize(context);
}

  构造方法进行了参数最终的赋值过程。将之前保存了图标基础大小等参数的InvariantDeviceProfile对象,赋给成员对象 this.inv = inv; 留待后面取用。如:allAppsIconTextSizeSp 就是直接取的inv对象中的数值进行赋值。

allAppsIconTextSizeSp = inv.iconTextSize

  后面就是通过updateAvailableDimensions(dm, res)和computeAllAppsButtonSize(context)进行一些赋值操作,如下,就不细说了。

iconSizePx = (int) (Utilities.pxFromDp(inv.iconSize, dm) * scale); iconTextSizePx = (int) (Utilities.pxFromSp(inv.iconTextSize, dm) * scale);

  初始化完成后,本文最开始通过app.getInvariantDeviceProfile().portraitProfile返回的就是该实例对象。 Controller 层就可以通过该对象获取到显示的相关参数。 2、根据 mDeviceProfile 对象中保存的数据调整布局参数

  通过传入Launcher的实例,来获取显示控件的实例对象,最后通过修改对应的 LayoutParams 进行布局的调整。   可以看到,这里对SearchDropTargetBar、Workspace、Hotseat、PageIndicator和长按桌面显示的OverviewPanel都进行了布局调整。这些控件的介绍详见 Launcher3源码分析(2)——UI显示控件的组成

public class DeviceProfile { ......

public void layout(Launcher launcher) {
    FrameLayout.LayoutParams lp;
    boolean hasVerticalBarLayout = isVerticalBarLayout();
    final boolean isLayoutRtl = Utilities.isRtl(launcher.getResources());

    // Layout the search bar space
    Rect searchBarBounds = getSearchBarBounds(isLayoutRtl);
    View searchBar = launcher.getSearchDropTargetBar();
    lp = (FrameLayout.LayoutParams) searchBar.getLayoutParams();
    lp.width = searchBarBounds.width();
    lp.height = searchBarBounds.height();
    lp.topMargin = searchBarTopExtraPaddingPx;
    if (hasVerticalBarLayout) {
        lp.gravity = Gravity.LEFT;

        LinearLayout targets = (LinearLayout) searchBar.findViewById(R.id.drag_target_bar);
        targets.setOrientation(LinearLayout.VERTICAL);
        FrameLayout.LayoutParams targetsLp = (FrameLayout.LayoutParams) targets.getLayoutParams();
        targetsLp.gravity = Gravity.TOP;
        targetsLp.height = LayoutParams.WRAP_CONTENT;

    } else {
        // Horizontal search bar space
        lp.gravity = Gravity.TOP|Gravity.CENTER_HORIZONTAL;
    }
    searchBar.setLayoutParams(lp);

    // Layout the workspace
    PagedView workspace = (PagedView) launcher.findViewById(R.id.workspace);
    lp = (FrameLayout.LayoutParams) workspace.getLayoutParams();
    lp.gravity = Gravity.CENTER;
    Rect padding = getWorkspacePadding(isLayoutRtl);
    workspace.setLayoutParams(lp);
    workspace.setPadding(padding.left, padding.top, padding.right, padding.bottom);
    workspace.setPageSpacing(getWorkspacePageSpacing(isLayoutRtl));

    // Layout the hotseat
    View hotseat = launcher.findViewById(R.id.hotseat);
    lp = (FrameLayout.LayoutParams) hotseat.getLayoutParams();
    float workspaceCellWidth = (float) getCurrentWidth() / inv.numColumns;
    float hotseatCellWidth = (float) getCurrentWidth() / inv.numHotseatIcons;
    int hotseatAdjustment = Math.round((workspaceCellWidth - hotseatCellWidth) / 2);
    if (hasVerticalBarLayout) {
        // Vertical hotseat -- The hotseat is fixed in the layout to be on the right of the
        //                     screen regardless of RTL
        lp.gravity = Gravity.RIGHT;
        lp.width = normalHotseatBarHeightPx;
        lp.height = LayoutParams.MATCH_PARENT;
        hotseat.findViewById(R.id.layout).setPadding(0, 2 * edgeMarginPx, 0, 2 * edgeMarginPx);
    } else if (isTablet) {
        // Pad the hotseat with the workspace padding calculated above
        lp.gravity = Gravity.BOTTOM;
        lp.width = LayoutParams.MATCH_PARENT;
        lp.height = hotseatBarHeightPx;
        hotseat.findViewById(R.id.layout).setPadding(
                hotseatAdjustment + padding.left, 0,
                hotseatAdjustment + padding.right, 2 * edgeMarginPx);
    } else {
        // For phones, layout the hotseat without any bottom margin
        // to ensure that we have space for the folders
        lp.gravity = Gravity.BOTTOM;
        lp.width = LayoutParams.MATCH_PARENT;
        lp.height = hotseatBarHeightPx;
        hotseat.findViewById(R.id.layout).setPadding(
                hotseatAdjustment + padding.left, 0,
                hotseatAdjustment + padding.right, 0);
    }
    hotseat.setLayoutParams(lp);

    // Layout the page indicators
    View pageIndicator = launcher.findViewById(R.id.page_indicator);
    if (pageIndicator != null) {
        if (hasVerticalBarLayout) {
            // Hide the page indicators when we have vertical search/hotseat
            pageIndicator.setVisibility(View.GONE);
        } else {
            // Put the page indicators above the hotseat
            lp = (FrameLayout.LayoutParams) pageIndicator.getLayoutParams();
            lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;
            lp.width = LayoutParams.WRAP_CONTENT;
            lp.height = LayoutParams.WRAP_CONTENT;
            lp.bottomMargin = hotseatBarHeightPx;
            pageIndicator.setLayoutParams(lp);
        }
    }

    // Layout the Overview Mode
    ViewGroup overviewMode = launcher.getOverviewPanel();
    if (overviewMode != null) {
        int overviewButtonBarHeight = getOverviewModeButtonBarHeight();
        lp = (FrameLayout.LayoutParams) overviewMode.getLayoutParams();
        lp.gravity = Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM;

        int visibleChildCount = getVisibleChildCount(overviewMode);
        int totalItemWidth = visibleChildCount * overviewModeBarItemWidthPx;
        int maxWidth = totalItemWidth + (visibleChildCount-1) * overviewModeBarSpacerWidthPx;

        lp.width = Math.min(availableWidthPx, maxWidth);
        lp.height = overviewButtonBarHeight;
        overviewMode.setLayoutParams(lp);

        if (lp.width > totalItemWidth && visibleChildCount > 1) {
            // We have enough space. Lets add some margin too.
            int margin = (lp.width - totalItemWidth) / (visibleChildCount-1);
            View lastChild = null;

            // Set margin of all visible children except the last visible child
            for (int i = 0; i < visibleChildCount; i++) {
                if (lastChild != null) {
                    MarginLayoutParams clp = (MarginLayoutParams) lastChild.getLayoutParams();
                    if (isLayoutRtl) {
                        clp.leftMargin = margin;
                    } else {
                        clp.rightMargin = margin;
                    }
                    lastChild.setLayoutParams(clp);
                    lastChild = null;
                }
                View thisChild = overviewMode.getChildAt(i);
                if (thisChild.getVisibility() != View.GONE) {
                    lastChild = thisChild;
                }
            }
        }
    }
}

}

扩展

这边列举了初始化的一些显示相关参数。

public int numRows; //桌面每行图标数
public int numColumns; //桌面每列图标数
// Device properties in current orientation
public final int availableWidthPx; //屏幕可用宽度
public final int availableHeightPx; //屏幕可用高度
// Workspace
private final int pageIndicatorHeightPx; //指示器高度
// Workspace icons
public int iconSizePx; //图标大小
public int iconTextSizePx; //图标文字大小
// Folder
public int folderIconSizePx;
public int folderCellWidthPx;
public int folderCellHeightPx;
// Hotseat
public int hotseatCellWidthPx;
public int hotseatCellHeightPx;
public int hotseatIconSizePx;
// All apps
public int allAppsButtonVisualSize; //allApp入口按钮大小
public final int allAppsIconSizePx;
public final float allAppsIconTextSizeSp;

如果想修改图标默认大小。一般有两种方式,

修改InvariantDeviceProfile对象中图标原始基础大小:成员变量 iconSize
修改DeviceProfile对象中最终生效的图标大小:成员变量 iconSizePx
                    

原文链接:blog.csdn.net/kuaiguixs/a…