小松的技术博客

六和敬

若今生迷局深陷,射影含沙。便许你来世袖手天下,一幕繁华。 你可愿转身落座,掌间朱砂,共我温酒煮茶。

Android开发中单位dp到px的转换

一、开发中遇到的一个坑

首先先叙述一个近期开发遇到的一个坑,由此启动了自己在单位转换上深入的探索。

开发需求是一个我们需要为一个view增加一条边线,我们采取区inset扩大背景然后包shape方案,具体如下:

<inset xmlns:android="http://schemas.android.com/apk/res/android"
    android:insetLeft="-2dp"
    android:insetRight="-2dp"
    android:insetTop="-2dp" >
    <shape>
        <solid android:color="@color/review_item_abstract_and_from_bg_pressed" />
        <stroke
            android:width="2dp"
            android:color="#ccc" />
        </shape>
</inset>

其就是想通过扩大左、右、上方的背景区域来隐藏shape的三边使得只有一边暴露出来,这个方案初看是完美的,在一般的手机上也没有问题,可在我的魅族手机上一看,上、左、右都冒出了1px线条,这是这个忧伤的故事的开端。看到了bug,就要去探索其存在的原因,因而就费了很大精力去探索,然后还是得到了比较大得收获的。

二、一些基础知识

这里会涉及一些java数据类型强制转换的知识点,先罗列出来。 java中可以采用float数据前加(int)使之强制转换为int型数据,其转换规则基本上是很直接的去掉小数点和后面的部分,而不是四舍五入的规则:

(int)-3.7  --> -3
(int)-3.2  --> -3
(int)3.2   -->  3
(int)3.8  -->  3

但为了达到有四舍五入的实现,有为其数值加上0.5f,然后再调用java自身的强制转换,这也是目前市场上用dp转化为px以及px转化为dp的计算方法

import android.content.Context;

public class DensityUtil {
    public static int dip2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }
    public static int px2dip(Context context, float pxValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (pxValue / scale + 0.5f);
    }
}

但其实这在负值上也是一个坑,例如说-1.5dp,我的魅族手机是2.75倍屏,因而转换为px为(int)(-1.5*2.75+0.5f) –> (int)(-4.125+0.5f) –> (int)(-3.625) –> -3。这与想象中不一致吧,探索到这一步时,本以为问题就发生在这个环节,结果通过自己的计算和实际的现象依然不一致,因此继续探索了下去。

三、java代码获取res文件中dimens.xml的尺寸

android中的xml只是一种便于管理、阅读和加快开发速度的写法,在编译过程中都是转化为java代码进行执行的,因此不管是android系统还是开发者,都有需要去获取xml文件中定义的值,像android源码中view.java就有如下代码段:

case com.android.internal.R.styleable.View_background:
    background = a.getDrawable(attr);
    break;
case com.android.internal.R.styleable.View_padding:
    padding = a.getDimensionPixelSize(attr, -1);
    mUserPaddingLeftInitial = padding;
    mUserPaddingRightInitial = padding;
    leftPaddingDefined = true;
    rightPaddingDefined = true;
    break;
case com.android.internal.R.styleable.View_paddingLeft:
    leftPadding = a.getDimensionPixelSize(attr, -1);
    mUserPaddingLeftInitial = leftPadding;
    leftPaddingDefined = true;
    break;
case com.android.internal.R.styleable.View_paddingTop:
    topPadding = a.getDimensionPixelSize(attr, -1);
    break;
case com.android.internal.R.styleable.View_paddingRight:
    rightPadding = a.getDimensionPixelSize(attr, -1);
    mUserPaddingRightInitial = rightPadding;
    rightPaddingDefined = true;
    break;

上述代码就是判断xml文件中属性类型,然后采取不同的处理方式,比如和单位有关的getDimensionPixelSize()方法,在这里看到了这个方法,网上就去搜索相关的知识点,然后得到了xml文件中单位到java代码中的相关方法。

从xml文件到java代码中文件会有以下三种转换方法:

getDimension()
getDimensionPixelOffset()
getDimensionPixelSize()

这三个方法在TypeArray类和Resource类上都存在,功能类似,TypeArray类是用于自定义属性的,而Resource类用于系统内置属性。

关于上述三个方法,去查阅官方文档,获取最精准的描述(Resource类中):

  • getDimension: Retrieve a dimensional for a particular resource ID. Unit conversions are based on the current DisplayMetrics associated with the resources.

  • getDimensionPixelOffset: Retrieve a dimensional for a particular resource ID for use as an offset in raw pixels. This is the same as getDimension(int), except the returned value is converted to integer pixels for you. An offset conversion involves simply truncating the base value to an integer

  • getDimensionPixelSize: Retrieve a dimensional for a particular resource ID for use as a size in raw pixels. This is the same as getDimension(int), except the returned value is converted to integer pixels for use as a size. A size conversion involves rounding the base value, and ensuring that a non-zero base value is at least one pixel in size.

当然看英语描述也是很头痛的,它们的实现方法也挺复杂的,看不懂,但是很清楚地一件事情就是他是获取xml中得id后进行的计算,那也就可以不用关心实现过程而直接去看这三个方法产生的效果。因此直接在dimens.xml文件中写一堆值,然后在java中用getResources().getDimensionPixelOffset(R.dimen.nameValue)getResources().getDimensionPixelSize(R.dimen.nameValue)getResources().getDimension(R.dimen.nameValue)来获取值来看效果。测试了一些负值、正值,再根据网上解说和官方文档,可以给出简易的理解:

  • getDimension是直接获取转换后的float值而不做任何处理
  • getDimensionPixelSize是在getDimension获取值得基础上加上0.5f再强制转换为int,这个市场上得dp转px的计算方法一致,对于正数是四舍五入,对于负值来说小数部分大于0.5则向上取整而小数部分小于0.5则要向上取整再加1,略坑。
  • getDimensionPixelOffset是在getDimension获取的值后直接去掉小数部分的方式取整。

四、xml文件中不同类型的元素的单位转换

在获得上述知识点后,首先我自己去瞎推测xml中会用到哪一种转换形式,结果每一种都跟想象中得不一致,满满的都是忧伤,然后做了个猜测,就是不同的节点属性用了不同的转换形式,一开始觉得这不符合常理,这样岂不是会增加代码的混乱程度,所以还有点不敢,但是源码在手,也就有了勇气去看看了。

首先我们用到的inset、shape都属于drawable对象,所以直接去寻找对应的java对象吧,寻找对象也是挺重要的事情,找到了才能好好工作。

inset对应了InsetDrawable对象,那们就去InsetDrawable.java中寻找信息,结果如下:

case R.styleable.InsetDrawable_insetLeft:
    state.mInsetLeft = a.getDimensionPixelOffset(attr, state.mInsetLeft);
    break;
case R.styleable.InsetDrawable_insetTop:
    state.mInsetTop = a.getDimensionPixelOffset(attr, state.mInsetTop);
    break;
case R.styleable.InsetDrawable_insetRight:
    state.mInsetRight = a.getDimensionPixelOffset(attr, state.mInsetRight);
    break;
case R.styleable.InsetDrawable_insetBottom:
    state.mInsetBottom = a.getDimensionPixelOffset(attr, state.mInsetBottom);
    break;

从源码中中可以看到,inset里面是利用getDimensionPixelOffset来进行单位转换的。

然后继续去寻找shape对应的对象,一开始直接去搜索ShapeDrawable,结果在里面连stroke的影子都没有,只得网上求助,没有搜到直接的答案,所以直接搜了下“如何用java代码画一个shape”,然后冒出来了个GradientDrawable,才知道,xml中得shape的对象是它呀,差点受到了惊吓,那就去这个对象里面搜寻stroke得相关方法,结果如下:

final int defaultStrokeWidth = Math.max(0, st.mStrokeWidth);
final int width = a.getDimensionPixelSize(R.styleable.GradientDrawableStroke_width, defaultStrokeWidth);
final float dashWidth = a.getDimension(R.styleable.GradientDrawableStroke_dashWidth, st.mStrokeDashWidth);

ColorStateList colorStateList = a.getColorStateList(R.styleable.GradientDrawableStroke_color);
if (colorStateList == null) {
    colorStateList = st.mStrokeColorStateList;
}

if (dashWidth != 0.0f) {
    final float dashGap = a.getDimension(R.styleable.GradientDrawableStroke_dashGap, st.mStrokeDashGap);
    setStroke(width, colorStateList, dashWidth, dashGap);
} else {
    setStroke(width, colorStateList);
}

从源码中可以看到,shape中stroke用的时getDimensionPixelSize来转换单位,竟然真的和inset的不一致,顺便还看到了shape中得padding用的时getDimensionPixelOffset

到这里,这次遇到的bug的原因终于完全清晰了,同时也知道了xml到java代码中单位转换的很多细节,哭泣一会儿,然后开心的工作。

最后回到那个bug本身,由于两个元素在单位转换时策略的不同,因此在不同的屏幕密度的手机上正负值不能做到完全的抵消,但转化后的结果也只有两种情况,一种是刚好抵消,一种是相差了1像素,所以保险的做法就是在在inset的负值上多减去0.5dp,开始文章就变成-2.5dp,这样就消灭这个bug了。

←支付宝← →微信 →
comments powered by Disqus