1.问题引入


在项目中遇到这么一个问题,大致是说Settings下的Battery各耗电子项的图片ICON分辨率太低。




Android图片应该放在哪个文件夹 安卓图片存放位置_资源



由于图片是黑白的,加上分辨率可能确实是低一点,和彩色的ICON比起来,是显得模糊一些。于是尝试定位对应代码,试图找到其使用的资源名称。

首先,根据菜单项的路径深度来查询。Settings首界面由dashboard_categories.xml文件定义,其中battery对应的是PowerUsageSummary,它是一个Fragment。

<!-- Battery -->
        <dashboard-tile
                android:id="@+id/battery_settings"
                android:title="@string/power_usage_summary_title"
                android:fragment="com.android.settings.fuelgauge.PowerUsageSummary"
                android:icon="@drawable/ic_settings_battery"
                />


再跟踪PowerUsageSummary,发现它对应的布局文件为power_usage_summary.xml,它由3部分大块组成:一个控制状态栏显示电池百分比的开关,一个自定义的电池耗电曲线图,以及接下来我们要找的各子项耗电列表。

/packages/apps/Settings/res/xml/power_usage_summary.xml

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
                  xmlns:settings="http://schemas.android.com/apk/res/com.android.settings"
        android:title="@string/power_usage_summary_title"
        settings:keywords="@string/keywords_battery">
        <!-- add 20160910 by xxx for 2823091/814330 begin -->
         <CheckBoxPreference
             android:key="battery_status_percentage"
             android:persistent="false"
             android:title="@string/battery_status_percentage" />
        <!-- add 20160910 by xxx for 2823091/814330 end -->
        <com.android.settings.fuelgauge.BatteryHistoryPreference
            android:key="battery_history" />
        <PreferenceCategory
            android:key="app_list"
            android:title="@string/power_usage_list_summary" />
</PreferenceScreen>


在PowerUsageSummary的refreshStats()方法中,有往app_list中添加Preference类,每行对应的Preference是PowerGaugePreference,再看图片来源于BatteryEntry。

/packages/apps/Settings/src/com/android/settings/fuelgauge/PowerUsageSummary.java

protected void refreshStats() {
        super.refreshStats();
        updatePreference(mHistPref);
        mAppListGroup.removeAll();
        mAppListGroup.setOrderingAsAdded(false);
        boolean addedSome = false;

        final PowerProfile powerProfile = mStatsHelper.getPowerProfile();
        final BatteryStats stats = mStatsHelper.getStats();
        final double averagePower = powerProfile.getAveragePower(PowerProfile.POWER_SCREEN_FULL);

        TypedValue value = new TypedValue();
        getContext().getTheme().resolveAttribute(android.R.attr.colorControlNormal, value, true);
        int colorControl = getContext().getColor(value.resourceId);

        if (averagePower >= MIN_AVERAGE_POWER_THRESHOLD_MILLI_AMP || USE_FAKE_DATA) {
            final List<BatterySipper> usageList = getCoalescedUsageList(
                    USE_FAKE_DATA ? getFakeStats() : mStatsHelper.getUsageList());

            ......
                if (sipper.drainType == BatterySipper.DrainType.OVERCOUNTED) {
                    // Don't show over-counted unless it is at least 2/3 the size of
                    // the largest real entry, and its percent of total is more significant
                    if (sipper.totalPowerMah < ((mStatsHelper.getMaxRealPower()*2)/3)) {
                        continue;
                    }
                    if (percentOfTotal < 10) {
                        continue;
                    }
                    if ("user".equals(Build.TYPE)) {
                        continue;
                    }
                }
                if (sipper.drainType == BatterySipper.DrainType.UNACCOUNTED) {
                    // Don't show over-counted unless it is at least 1/2 the size of
                    // the largest real entry, and its percent of total is more significant
                    if (sipper.totalPowerMah < (mStatsHelper.getMaxRealPower()/2)) {
                        continue;
                    }
                    if (percentOfTotal < 5) {
                        continue;
                    }
                    if ("user".equals(Build.TYPE)) {
                        continue;
                    }
                }
                final UserHandle userHandle = new UserHandle(UserHandle.getUserId(sipper.getUid()));
                final BatteryEntry entry = new BatteryEntry(getActivity(), mHandler, mUm, sipper);//那些原生的黑白ICON来源
                final Drawable badgedIcon = mUm.getBadgedIconForUser(entry.getIcon(),
                        userHandle);
                final CharSequence contentDescription = mUm.getBadgedLabelForUser(entry.getLabel(),
                        userHandle);
                final PowerGaugePreference pref = new PowerGaugePreference(getActivity(),
                        badgedIcon, contentDescription, entry);

                final double percentOfMax = (sipper.totalPowerMah * 100)
                        / mStatsHelper.getMaxPower();
                sipper.percent = percentOfTotal;
                pref.setTitle(entry.getLabel());
                pref.setOrder(i + 1);
                pref.setPercent(percentOfMax, percentOfTotal);
                if (sipper.uidObj != null) {
                    pref.setKey(Integer.toString(sipper.uidObj.getUid()));
                }
                if ((sipper.drainType != DrainType.APP || sipper.uidObj.getUid() == 0)
                         && sipper.drainType != DrainType.USER) {
                    pref.setTint(colorControl);
                }
                addedSome = true;
                mAppListGroup.addPreference(pref);//往app_list中添加耗电子项
                if (mAppListGroup.getPreferenceCount() > (MAX_ITEMS_TO_LIST + 1)) {
                    break;
                }
            }
        }
        if (!addedSome) {
            addNotAvailableMessage();
        }

        BatteryEntry.startRequestQueue();
    }


接下来查看BatteryEntry类中那些都有那些黑白ICON。

/packages/apps/Settings/src/com/android/settings/fuelgauge/BatteryEntry.java

public BatteryEntry(Context context, Handler handler, UserManager um, BatterySipper sipper) {
        sHandler = handler;
        this.context = context;
        this.sipper = sipper;
        switch (sipper.drainType) {
            case IDLE:
                name = context.getResources().getString(R.string.power_idle);
                iconId = R.drawable.ic_settings_phone_idle;
                break;
            case CELL:
                name = context.getResources().getString(R.string.power_cell);
                iconId = R.drawable.ic_settings_cell_standby;
                break;
            case PHONE:
                name = context.getResources().getString(R.string.power_phone);
                iconId = R.drawable.ic_settings_voice_calls;
                break;
            case WIFI:
                name = context.getResources().getString(R.string.power_wifi);
                iconId = R.drawable.ic_settings_wireless;
                break;
            case BLUETOOTH:
                name = context.getResources().getString(R.string.power_bluetooth);
                iconId = R.drawable.ic_settings_bluetooth;
                break;
            case SCREEN:
                name = context.getResources().getString(R.string.power_screen);
                iconId = R.drawable.ic_settings_display;
                break;
            case FLASHLIGHT:
                name = context.getResources().getString(R.string.power_flashlight);
                iconId = R.drawable.ic_settings_display;
                break;
            case APP:
                name = sipper.packageWithHighestDrain;
                break;
            case USER: {
                UserInfo info = um.getUserInfo(sipper.userId);
                if (info != null) {
                    icon = Utils.getUserIcon(context, um, info);
                    name = Utils.getUserLabel(context, info);
                } else {
                    icon = null;
                    name = context.getResources().getString(
                            R.string.running_process_item_removed_user_label);
                }
            } break;
            case UNACCOUNTED:
                name = context.getResources().getString(R.string.power_unaccounted);
                iconId = R.drawable.ic_power_system;
                break;
            case OVERCOUNTED:
                name = context.getResources().getString(R.string.power_overcounted);
                iconId = R.drawable.ic_power_system;
                break;
            case CAMERA:
                name = context.getResources().getString(R.string.power_camera);
                iconId = R.drawable.ic_settings_camera;
                break;
        }
        if (iconId > 0) {
            icon = context.getDrawable(iconId);
        }
        if ((name == null || iconId == 0) && this.sipper.uidObj != null) {
            getQuickNameIconForUid(this.sipper.uidObj.getUid());
        }
    }

  

有上面可知,其中可能现实的黑白ICON有,WIFI、蓝牙、屏幕、相机等。此时,已经找到需要修改替换的图片资源名称,接下来直接替换更高的分辨率的资源即可[1]。

         接下来,以ic_settings_bluetooth为例,其以R.drawable的形式访问的是一个xml文件。

/packages/apps/Settings/res/drawable/ic_settings_bluetooth.xml


[1]


<?xml version="1.0" encoding="utf-8"?>

<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
    android:src="@drawable/ic_settings_bluetooth_alpha"
    android:tint="?android:attr/colorAccent" />


所以,需要直接替换的图片资源为ic_settings_bluetooth_alpha.png。接下来需要找到以这个命名的图片,发现在很多不同文件夹下都有以此命名的图片,且分辨率各不相同。我们找到这是因为要适配不同屏幕做准备的,实现应用的可移植性,可以根据不同的像素密度,放置大小不一的图片,并且某一款实际的设备的像素的密度是固定的,其只对应某个文件夹中的确定图片资源。

 

Android图片应该放在哪个文件夹 安卓图片存放位置_Android图片应该放在哪个文件夹_02

         由API文档可知,从ldpi至xxxhdpi的比例大致遵循3:4:6:8:12:16的规则来存放图片资源。此时如果项替换图片,那就会产生一个疑问,究竟当前项目中使用的是哪个目录中的图片呢?


2.屏幕像素密度


据API文档,DisplayMetrics类中有种方法可以获取到屏幕的相关参数。

Astructure describing general information about a display, such as its size,density, and font scaling.

Toaccess the DisplayMetrics members, initialize an object like this:

 DisplayMetrics metrics = new DisplayMetrics();

 getWindowManager().getDefaultDisplay().getMetrics(metrics);

 

         但是Display中getMetrics方法的注释中明确说明,这个不代表当前设备的实际密度参数,而是可以由认为控制,以适配项目应用等的需要。不过此值不影响dpi文件夹资源定位。因为这些资源属于项目中使用的应用。

/frameworks/base/core/java/android/view/Display.java


/**
  * Gets display metrics that describe the size and density of this display.
 * <p>
 * The size is adjusted based on the current rotation of the display.
 * </p><p>
 * The size returned by this method does not necessarily represent the
 * actual raw size (native resolution) of the display.  The returned size may
 * be adjusted to exclude certain system decor elements that are always visible.
 * It may also be scaled to provide compatibility with older applications that
 * were originally designed for smaller displays.
 * </p>
 *
 * @param outMetrics A {@link DisplayMetrics} object to receive the metrics.
 */
public void getMetrics(DisplayMetrics outMetrics) {
    synchronized (this) {
        updateDisplayInfoLocked();
        mDisplayInfo.getAppMetrics(outMetrics, mDisplayAdjustments);
    }
}


接下来,自己写个简单的应用来读取项目中的dpi。


//strDPI = getResources().getDisplayMetrics().densityDpi;
        DisplayMetrics metrics = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(metrics);
        strDPI = metrics.densityDpi;
txtView.setText("desityDpi:"+String.valueOf(strDPI));


得到的结果如下面图片所示,根据 API

文档中的说法, 320dpi 应该是 xdpi 。为了印证这一结论,我们对不同文件夹下的同名图片资源做了修改,分辨率依然不变,通过画图工具,使得不同文件下的图片表现不一致,以示区分。


Android图片应该放在哪个文件夹 安卓图片存放位置_资源_03


经过验证,我们得到的实际结果如下图所示,确实是xhdpi,此结果已得到验证,接下来就可以取替换相关目录下的图片资源。



Android图片应该放在哪个文件夹 安卓图片存放位置_drawable_04



至此,分辨率低的问题已经得到解决。但是,于此同时,产生了这么一个疑问:前面提到这个desityDpi是可以认为定制的,并且可以与屏幕实际的dpi有出入,那这个值是怎么定制的呢?



3.dpi定制


回到屏幕像素密度获取方法中的DisplayMetrics类,densityDpi除了构造方法之外,就只剩setToDefaults()这一个方法可以初始化它。


/frameworks/base/core/java/android/util/DisplayMetrics.java


public void setToDefaults() {
        widthPixels = 0;
        heightPixels = 0;
        density =  DENSITY_DEVICE / (float) DENSITY_DEFAULT;
        densityDpi =  DENSITY_DEVICE;//初始化为某个静态变量所表示的值
        scaledDensity = density;
        xdpi = DENSITY_DEVICE;
        ydpi = DENSITY_DEVICE;
        noncompatWidthPixels = widthPixels;
        noncompatHeightPixels = heightPixels;
        noncompatDensity = density;
        noncompatDensityDpi = densityDpi;
        noncompatScaledDensity = scaledDensity;
        noncompatXdpi = xdpi;
        noncompatYdpi = ydpi;
    }


静态变量的声明如下,是来自于本类中的一个静态方法。


/**
     * The device's density.
     * @hide because eventually this should be able to change while
     * running, so shouldn't be a constant.
     * @deprecated There is no longer a static density; you can find the
     * density for a display in {@link #densityDpi}.
     */
    @Deprecated
    public static int DENSITY_DEVICE = getDeviceDensity();
	由方法可知,这个用户定制的屏幕像素密度来自一个系统属性。
    private static int getDeviceDensity() {
        // qemu.sf.lcd_density can be used to override ro.sf.lcd_density
        // when running in the emulator, allowing for dynamic configurations.
        // The reason for this is that ro.sf.lcd_density is write-once and is
        // set by the init process when it parses build.prop before anything else.
        return SystemProperties.getInt("qemu.sf.lcd_density",
                SystemProperties.getInt("ro.sf.lcd_density", DENSITY_DEFAULT));
    }
public static final int DENSITY_DEFAULT = DENSITY_MEDIUM;//160

通过项目手机查询,发现只有后面那个系统属性,且读出来的值为320。


user@swd:/local/sdb/xxx$ adb shell getprop | grep density
[ro.sf.lcd_density]: [320]

 

4.手机实际计算dpi


据手机厂商提供的数据来看,本测试手机的分辨率为1280*720的5吋屏:

Main LCD Size (inch)

5.2

Main display resolution

HD (1280×720)

         经过计算,得到的实际dpi为293.72,与定制的320还是有点差距。

 

(完)