1.问题引入
在项目中遇到这么一个问题,大致是说Settings下的Battery各耗电子项的图片ICON分辨率太低。
由于图片是黑白的,加上分辨率可能确实是低一点,和彩色的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。接下来需要找到以这个命名的图片,发现在很多不同文件夹下都有以此命名的图片,且分辨率各不相同。我们找到这是因为要适配不同屏幕做准备的,实现应用的可移植性,可以根据不同的像素密度,放置大小不一的图片,并且某一款实际的设备的像素的密度是固定的,其只对应某个文件夹中的确定图片资源。
由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
。为了印证这一结论,我们对不同文件夹下的同名图片资源做了修改,分辨率依然不变,通过画图工具,使得不同文件下的图片表现不一致,以示区分。
经过验证,我们得到的实际结果如下图所示,确实是xhdpi,此结果已得到验证,接下来就可以取替换相关目录下的图片资源。
至此,分辨率低的问题已经得到解决。但是,于此同时,产生了这么一个疑问:前面提到这个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还是有点差距。
(完)