Android M Launcher3屏幕适配

前言

我们知道Launcher3在不同的设备上,都能够很好的适配屏幕,包括桌面图标大小、字体大小、桌面及应用列表的行列数等。那么它是怎么做到的呢?

LauncherAppState初始化

要想知道Launcher3是如何做到屏幕适配的,我们首先从Launcher的初始化开始分析。我们知道Launcher的初始化是从Launcher.java的onCreate方法开始的,我们先看下onCreate最先做了什么事情。

    super.onCreate(savedInstanceState);
    LauncherAppState.setApplicationContext(getApplicationContext());
    LauncherAppState app = LauncherAppState.getInstance();
    
    ...

我们看到Launcher在onCreate方法开始首先初始化LauncherAppState,这一初始化过程主要做了两件事情,1)设置应用上下文;2)获取LauncherAppState的单例对象。在获取取LauncherAppState的单例对象过程中,如果LauncherAppState的单例对象不存在,则会初始化一个。在LauncherAppState的构造方法中有一系列的初始化。

private LauncherAppState() {
    if (sContext == null) {
        throw new IllegalStateException("LauncherAppState inited before app context set");
    }

    Log.v(Launcher.TAG, "LauncherAppState inited");

    if (sContext.getResources().getBoolean(R.bool.debug_memory_enabled)) {
        MemoryTracker.startTrackingMe(sContext, "L");
    }

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

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

    LauncherAppsCompat.getInstance(sContext).addOnAppsChangedCallback(mModel);

    // Register intent receivers
    IntentFilter filter = new IntentFilter();
    filter.addAction(Intent.ACTION_LOCALE_CHANGED);
    filter.addAction(SearchManager.INTENT_GLOBAL_SEARCH_ACTIVITY_CHANGED);
    // For handling managed profiles
    filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_ADDED);
    filter.addAction(LauncherAppsCompat.ACTION_MANAGED_PROFILE_REMOVED);

    sContext.registerReceiver(mModel, filter);
    UserManagerCompat.getInstance(sContext).enableAndResetCache();
    }

在上面代码中我们看到有mInvariantDeviceProfile的初始化,接下来我们分析下它是如何加载一些默认屏幕配置的。

InvariantDeviceProfile的初始化

从上面代码我们可以看到在LauncherAppState的构造方法中通过new InvariantDeviceProfile的方式得到一个mInvariantDeviceProfile对象,我们来看下这个new的过程中做了什么事情。先上代码,我们再一步一步分析。

InvariantDeviceProfile(Context context) {
        //获取WindowManager服务
        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);
        minHeightDps = Utilities.dpiFromPx(Math.min(largestSize.x, largestSize.y), dm);

        //通过最小宽高和预定义的配置文件获取一个最接近的配置文件列表
        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 */);
    }

通过上述代码我们可以看到首先通过context.getSystemService获取一个WindowMnager实例,WindowManager是继承自ViewManager的一个接口,其实现类是WindowManagerImpl.java。
WindowManager中存在成员变量Display,可以通过wm.getDefaultDisplay获取Display实例,其实DefaulDisplay就是手机的默认屏幕(其他的屏幕可以是通过HDMI连接的屏幕)。
获取默认屏幕之后,根据屏幕最小宽高之后,从px转换成Dp,得到minWidthDps,minHeightDps这两个变量。然后根据最小的宽、高及预定义的配置文件getPredefinedDeviceProfiles(),得到一个最接近的配置文件列表。我们先来看下这个预定义的配置文件是什么?

预定义配置文件

其实这个预定义的配置文件,是google默认添加的一些设备Launcher的具体显示参数。如下所示:

 predefinedDeviceProfiles.add(new InvariantDeviceProfile("Nexus S",
                296, 491.33f, 4, 4, 4, 4, 4, 48, 13, 5, 48, R.xml.default_workspace_4x4));

这条配置信息就是显示的Nexus S的Launcher显示的具体参数,依次为:最小的宽296、高491.33,桌面列表图标的行4、列数4,文件夹中图标的行4、列4,应用列表中图标的预设行数4,图标的大小48px,图标下方字体大小13px,dock栏图标个数及图标大小,最后一个xml是默认桌面的配置文件。
通过上边的一条配置信息我们就可以看出它规定了桌面显示的个个具体信息。有了这些显示的具体信息我们是如何得到最接近的配置列表的呢?

获取与当前屏幕最近进的配置文件

其实获取最接近的配置文件很简单就是通过最小的宽高和预制进去的宽高做一个平方和的平方根得到一个值,根据这个值讲预制的列表做一个从小到大的排序。

//平方和的平方根运算
float dist(float x0, float y0, float x1, float y1) {
        return (float) Math.hypot(x1 - x0, y1 - y0);
}

//通过排序得到一个最近配置列表,最接近的排在最前边
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 (int) (dist(width, height, a.minWidthDps, a.minHeightDps)
                        - dist(width, height, b.minWidthDps, b.minHeightDps));
            }
        });

        return pointsByNearness;
    }

配置当前屏幕的显示参数

得到最接近的配置列表(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;

通过上边的代码我们发现并不是通过closestProfile设置图标及图标字体的大小。而是另外的配置文件interpolatedDeviceProfileOut。

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

那么这个interpolatedDeviceProfileOut是从哪里来的呢?我们回到上边InvariantDeviceProfile初始化的代码。我们可以看到InvariantDeviceProfile又是通过一些插值运算得到的。

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);
    }

我们可以简单理解为当我们的屏幕无法在预制列表中找到最佳(屏幕完全一致)配置时,为了找到最合适显示当前屏幕的图标的大小,我们需要找到接近的(KNEARESTNEIGHBOR)三个配置,然后通过一个加权运算(weight(width, height, p.minWidthDps, p.minHeightDps, WEIGHT_POWER);)得到一个平均值,以达到最合适的图标及图标字体大小。至此Launcher已经完成了显示的一系列参数,最后我们再回到closestProfile.minAllAppsPredictionColumns,这是一个预设的参数,在实际的显示中会根据图标的大小由launcher动态调整。

总结

我们看到Launcher的屏幕适配其实就是得到一些预制的配置参数,通过计算得到一个最接近的配置文件,通过该配置文件为当前屏幕的显示做一些参数设置,已达到适配的目的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,398评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,585评论 18 139
  • Ubuntu的发音 Ubuntu,源于非洲祖鲁人和科萨人的语言,发作 oo-boon-too 的音。了解发音是有意...
    萤火虫de梦阅读 99,119评论 9 467
  • 沪杭车中 匆匆匆!催催催! 一卷烟,一片山,几点云影, 一道水,一条桥,一支橹声, 一林松,一丛竹,红叶纷纷: 艳...
    舒嘉仪阅读 525评论 0 0
  • 1、初中时候,一个平时特严厉的老师正在黑板抄题,说了一句不好意思抄错行了。正当班级静静的时候,二货同桌因为我踩到了...
    冬天的一把火i阅读 214评论 0 0