Android的手机屏幕,不管是分辨率还是大小都是五花无门、千奇百怪的,这在一定程度上造成了与UI相关工作开发的困难.所以,了解屏幕相关的知识,就显得很重要。
图中很多参数的设置,都将影响到App的样式与主题。其中:colorPrimary对应ActionBar的颜色;colorParmaryDark对应状态栏的颜色;colorAccent对应EditText编辑时、RadioButton选中、CheckBox等选中时的颜色。另,对于Android 5.0(api 21)以下的设备,目前colorParmaryDark无法去个性化状态栏的颜色;底部的navagationBar可能也不一样(更别说设置颜色了)。这些属性的具体使用,我会抽空进行补充。
一块屏幕通常具备以下的几个参数:
1.屏幕大小
指屏幕对角线的长度,通常用"寸"来表示,例如4.7寸手机、5.5寸手机等(1寸=2.54厘米)。
2.分辨率
(1)含义:分辨率是指实际屏幕的像素点个数,例如我们经常说的720X1280就是指屏幕的分辨率,宽有720个像素点,高有1280个像素点。
(2)单位:px(pixel)。1px=1像素点。UI设计师的设计图会以px作为统一的计量单位。
3.PPI
每英寸像素(Pixels Per Inch)又称为DPI(Dots Per Inch)。它是由对角线的像素点数除以屏幕的大小所得,通常有400PPI就已经是非常高的屏幕密度了。
每个厂商的安卓手机具有不同的大小尺寸和像素密度的屏幕。Android系统如果要精确到每种DPI的屏幕,那基本上是不可能的。因此,系统定义了几个标准的DPI值,作为手机的固定DPI,如下表所示:
密度类型 | 代表的分辨率(px) | 屏幕像素密度(dpi)范围 | app的图标对应分辨率(px) |
---|---|---|---|
中密度(mdpi) | 320x480 | 120~160 | 48x48 |
高密度(hdpi) | 480x800 | 160~240 | 72x72 |
超高密度(xhdpi) | 720x1280 | 240~320 | 96x96 |
超超高密度(xxhdpi) | 1080x1920 | 320~480 | 144x144 |
超超超高密度(xxxhdpi) | 2160x3840 | 480~640 | 192x192 |
(注:其实之前还有个ldpi,但是随着移动设备配置的不断升级,这个像素密度的设备已经很罕见了,所以现在适配时不需考虑)
mdpi、hdpi、xdpi、xxdpi、xxxhdpi用来修饰Android中的drawable文件夹及values文件夹,用来区分不同像素密度下的图片和dimen值。在进行开发的时候,我们将UI设计师切好的图(进行相同的命名后)放在合适的文件夹中即可。当然,绝大多数公司只会切一套图给你,如果是这样,你问问UI设计师她的设计分辨率是基于多少的.比如是7201280的,那你就把切好的图片放到drawable-xhdpi文件件中即可(如果她设计的是480800,那你将图片放在drawable-hdpi下.以我个人的经验,UI的分辨率一般都是750*1334的尺寸.这是ios 6/6s/7/8对应的分辨率.此时你就直接放在drawable-xhdpi)。另外,说一句也不少见的题外话:有些切图的人员会给你带有@2x命名的切图,你直接和他沟通让他重切。这是ios的切图,她只是在偷懒。要不然的话,你一个一个图片重新命名,会占用你大量宝贵的时间。
独立像素密度,有的人也翻译为:密度无关像素。正是由于各种屏幕密度的不同,导致同样像素大小的长度,在不同密度的屏幕上显示长度不同,因此相同长度的屏幕,高密度的屏幕包含更多的像素点,在安卓系统中使用mdpi密度值为160的屏幕作为标准,在这个屏幕上,1px = 1dp,其他屏幕则可以通过比例进行换算,例如同样是100dp的长度,mdpi中为100px,而在hdpi中为150,我们也可以得出在各个密度值中的换算公式: 在mdpi中 1dp = 1px; 在hdpi中, 1dp = 1.5px;在xhdpi中,1dp = 2px;在xxhdpi中1dp = 3px;在Xxxhdpi中1dp = 4px由此可见,我们换算公式 m:h:xh:xxh:xxxh = 3:4:6:8:12。
独立像素密度的意义:Android开发时用dp而不是px单位设置图片大小,是Android特有的单位。使用dp可以保证在不同屏幕像素密度的设备上显示相同的效果。
说直白一点,同一套代码,30dp在不同分辨手机上,用尺进行量,长度是一样的.
scale-independent pixel,叫sp或sip。开发者文档上的描述是这样的:Scale-independent Pixels – This is like the dp unit, but it is also scaled by the user’s font size preference. It is recommend you use this unit when specifying font sizes, so they will be adjusted for both the screen density and the user’s preference.
大致意思是:(1)sp除了受屏幕密度影响外,还受到用户的字体大小影响;(2)通常情况下,建议使用sp来跟随用户字体大小设置。(如果你觉得自己英语菜得不行,但又必须看英文文档时,那就下载一个有道词典,把一行英文高亮,然后鼠标右键中文马上就出现了。虽然翻译工具很多时间不准确,但总比你逐个单词查看强。)
Android开发时用此单位设置文字大小,可根据字体大小首选项进行缩放。推荐使用12sp、14sp、18sp、22sp作为字体设置的大小,不推荐使用奇数和小数,容易造成精度的丢失问题;小于12sp的字体会太小导致用户看不清。
代码如下:
TextView的内容除了一个使用sp,一个使用dp外,其他完全相同,我们来看看运行的效果:
通过上述三张图片可以看到:dp为单位的字体没有随系统字体大小的设置而改变,但sp的却改变了。因此通常情况下,我们还是建议使用sp作为字体的单位,除非一些特殊的情况,不想跟随系统字体变化的,可以使用dp(比如切图只有1套,图片和字体摆放在一起,要求图片与字体上下同一条线。这个时候,字体的单位也需要使用dp)。
在用户设置字体为标注时,1sp=1dp.并且同一套代码的1sp大小,在不同分辨率手机下,用尺量的大小也是一样长.
通过上述知识的分析,发现很多单位之间是可以转化的。而在开发过程中,有时又需要进行单位的换算。我们通常的做法是将换算的订单作为工具类保存在项目中。代码如下(文末的源码中也有):
public class DisplayUtils {
/**
* 将px值转换成dpi或者dp值,保持尺寸不变
*
* @param content
* @param pxValus
* @return
*/
public static int px2dip(Context content, float pxValus) {
final float scale = content.getResources().getDisplayMetrics().density;
return (int) (pxValus / scale + 0.5f);
}
/**
* 将dip和dp转化成px,保证尺寸大小不变。
*
* @param content
* @param pxValus
* @return
*/
public static int dip2px(Context content, float pxValus) {
final float scale = content.getResources().getDisplayMetrics().density;
return (int) (pxValus * scale + 0.5f);
}
/**
* 将px转化成sp,保证文字大小不变。
*
* @param content
* @param pxValus
* @return
*/
public static int px2sp(Context content, float pxValus) {
final float fontScale = content.getResources().getDisplayMetrics().scaledDensity;
return (int) (pxValus / fontScale + 0.5f);
}
/**
* 将sp转化成px,保证文字大小不变。
*
* @param content
* @param pxValus
* @return
*/
public static int sp2px(Context content, float pxValus) {
final float fontScale = content.getResources().getDisplayMetrics().scaledDensity;
return (int) (pxValus * fontScale + 0.5f);
}
}
其实的density就是前面所说的换算比例,这里使用的是公式换算方法进行转换。同时,系统也提供了TypedValue类帮助我们转换,代码如下:
/**
* dp2px
* @param dp
* @return
*/
protected int dp2px(int dp){
return (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dp,getResources().getDisplayMetrics());
}
/**
* sp2px
* @param dp
* @return
*/
protected int sp2px(int sp){
return (int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,sp,getResources().getDisplayMetrics());
}
屏幕的宽、高,状态栏、导航栏的宽高等,在开发过程中,尤其是涉及到UI的开发中,使用较多,我们一般也是封装到工具类,基本思路都是获取系统的属性。代码如下:
/**
* 获取屏幕的高度
* @param context
* @return
*/
public static int getScreenHeight(Context context) {
return context.getResources().getDisplayMetrics().heightPixels;
}
/**
* 获得状态栏的高度
* 比较常见的两种方式是:一是通过系统尺寸资源获取,二是通过反射获取
* @param context
* @return
*/
public static int getStatusHeight(Context context) {
int statusBarHeight = -1;
//获取status_bar_height资源的ID
int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
//根据资源ID获取响应的尺寸值
statusBarHeight = context.getResources().getDimensionPixelSize(resourceId);
}
return statusBarHeight;
}
/**
* 获取导航栏高度
* @param context
* @return
*/
public static int getNavitionBarHeight(Context context) {
int result = 0;
int resourceId=0;
int rid = context.getResources().getIdentifier("config_showNavigationBar", "bool", "android");
if (rid!=0){
resourceId = context.getResources().getIdentifier("navigation_bar_height", "dimen", "android");
return context.getResources().getDimensionPixelSize(resourceId);
}else
return 0;
}
另外,获取控件的属性,代码如下:
mTvTest.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
int mTvTestHeight = mTvTest.getHeight(); //控件的高度
int mTvTestWidth = mTvTest.getWidth(); //控件的宽度
}
});
屏幕的截图,尤其是涉及到分享时使用比较多。当前界面的截图,网上的代码比较多,没什么好说明的,代码如下:
/**
* 获取当前屏幕截图,包含状态栏
*
* @param activity
* @return
*/
public static Bitmap snapShotWithStatusBar(Activity activity) {
View view = activity.getWindow().getDecorView();
view.setDrawingCacheEnabled(true);
view.buildDrawingCache();
Bitmap bmp = view.getDrawingCache();
int width = getScreenWidth(activity);
int height = getScreenHeight(activity);
Bitmap bp = null;
bp = Bitmap.createBitmap(bmp, 0, 0, width, height);
view.destroyDrawingCache();
return bp;
}
/**
* 获取当前屏幕截图,不包含状态栏
*
* @param activity
* @return
*/
public static Bitmap snapShotWithoutStatusBar(Activity activity) {
View view = activity.getWindow().getDecorView();
view.setDrawingCacheEnabled(true);
view.buildDrawingCache();
Bitmap bmp = view.getDrawingCache();
Rect frame = new Rect();
activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(frame);
int statusBarHeight = frame.top;
int width = getScreenWidth(activity);
int height = getScreenHeight(activity);
Bitmap bp = null;
bp = Bitmap.createBitmap(bmp, 0, statusBarHeight, width, height
- statusBarHeight);
view.destroyDrawingCache();
return bp;
}
但,如果界面一屏展示不完,一般我们会用到 ListView ,ScrollView或Recyclerview。这时,应该如何实现界面的:
1.ScrollView的截屏
三个截屏中,ScrollView最简单,因为ScrollView只有一个childView,虽然没有全部显示在界面上,但是已经全部渲染绘制,因此可以直接 调用scrollView.draw(canvas)
来完成截图。代码如下:
/**
* 三个截屏中,ScrollView最简单,因为ScrollView只有一个childView,虽然没有全部显示在界面上,
* 但是已经全部渲染绘制,因此可以直接 调用`scrollView.draw(canvas)`来完成截图,
*/
/**
* http://blog.csdn.net/lyy1104/article/details/40048329
*/
public static Bitmap shotScrollView(ScrollView scrollView) {
int h = 0;
Bitmap bitmap = null;
for (int i = 0; i < scrollView.getChildCount(); i++) {
h += scrollView.getChildAt(i).getHeight();
scrollView.getChildAt(i).setBackgroundColor(Color.parseColor("#ffffff"));
}
bitmap = Bitmap.createBitmap(scrollView.getWidth(), h, Bitmap.Config.RGB_565);
final Canvas canvas = new Canvas(bitmap);
scrollView.draw(canvas);
return bitmap;
}
2.ListView的截屏
ListView就是会回收与重用Item,并且只会绘制在屏幕上显示的ItemView,根据stackoverflow上大神的建议,采用一个List来存储Item的视图,这种方案依然不够好,当Item足够多的时候,可能会发生oom。不过也没关系,RecyclerView已经够强大了,我们直接使用RecyclerView就好。我们还是看看ListView的截屏代码:
/**
* 而ListView就是会回收与重用Item,并且只会绘制在屏幕上显示的ItemView,根据stackoverflow上大神的建议,
* 采用一个List来存储Item的视图,这种方案依然不够好,当Item足够多的时候,可能会发生oom。
*/
/**
* http://stackoverflow.com/questions/12742343/android-get-screenshot-of-all-listview-items
*/
public static Bitmap shotListView(ListView listview) {
ListAdapter adapter = listview.getAdapter();
int itemscount = adapter.getCount();
int allitemsheight = 0;
List bmps = new ArrayList();
for (int i = 0; i < itemscount; i++) {
View childView = adapter.getView(i, null, listview);
childView.measure(
View.MeasureSpec.makeMeasureSpec(listview.getWidth(), View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
childView.layout(0, 0, childView.getMeasuredWidth(), childView.getMeasuredHeight());
childView.setDrawingCacheEnabled(true);
childView.buildDrawingCache();
bmps.add(childView.getDrawingCache());
allitemsheight += childView.getMeasuredHeight();
}
Bitmap bigbitmap =
Bitmap.createBitmap(listview.getMeasuredWidth(), allitemsheight, Bitmap.Config.ARGB_8888);
Canvas bigcanvas = new Canvas(bigbitmap);
Paint paint = new Paint();
int iHeight = 0;
for (int i = 0; i < bmps.size(); i++) {
Bitmap bmp = bmps.get(i);
bigcanvas.drawBitmap(bmp, 0, iHeight, paint);
iHeight += bmp.getHeight();
bmp.recycle();
bmp = null;
}
return bigbitmap;
}
3.RecyclerView的截屏
我们都知道,在新的Android版本中,已经可以用RecyclerView来代替使用ListView的场景,相比较ListView,RecyclerView对Item View的缓存支持的更好。可以采用和ListView相同的方案,这里也是在stackoverflow上看到的方案。相比ListView,RecyclerView不止好用那么一点点:
/**
* 我们都知道,在新的Android版本中,已经可以用RecyclerView来代替使用ListView的场景,相比较ListView,
* RecyclerView对Item View的缓存支持的更好。可以采用和ListView相同的方案,这里也是在stackoverflow上看到的方案。
*/
/**
* https://gist.github.com/PrashamTrivedi/809d2541776c8c141d9a
*/
public static Bitmap shotRecyclerView(RecyclerView view) {
RecyclerView.Adapter adapter = view.getAdapter();
Bitmap bigBitmap = null;
if (adapter != null) {
int size = adapter.getItemCount();
int height = 0;
Paint paint = new Paint();
int iHeight = 0;
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// Use 1/8th of the available memory for this memory cache.
final int cacheSize = maxMemory / 8;
LruCache bitmaCache = new LruCache<>(cacheSize);
for (int i = 0; i < size; i++) {
RecyclerView.ViewHolder holder = adapter.createViewHolder(view, adapter.getItemViewType(i));
adapter.onBindViewHolder(holder, i);
holder.itemView.measure(
View.MeasureSpec.makeMeasureSpec(view.getWidth(), View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
holder.itemView.layout(0, 0, holder.itemView.getMeasuredWidth(),
holder.itemView.getMeasuredHeight());
holder.itemView.setDrawingCacheEnabled(true);
holder.itemView.buildDrawingCache();
Bitmap drawingCache = holder.itemView.getDrawingCache();
if (drawingCache != null) {
bitmaCache.put(String.valueOf(i), drawingCache);
}
height += holder.itemView.getMeasuredHeight();
}
bigBitmap = Bitmap.createBitmap(view.getMeasuredWidth(), height, Bitmap.Config.ARGB_8888);
Canvas bigCanvas = new Canvas(bigBitmap);
Drawable lBackground = view.getBackground();
if (lBackground instanceof ColorDrawable) {
ColorDrawable lColorDrawable = (ColorDrawable) lBackground;
int lColor = lColorDrawable.getColor();
bigCanvas.drawColor(lColor);
}
for (int i = 0; i < size; i++) {
Bitmap bitmap = bitmaCache.get(String.valueOf(i));
bigCanvas.drawBitmap(bitmap, 0f, iHeight, paint);
iHeight += bitmap.getHeight();
bitmap.recycle();
}
}
return bigBitmap;
}
1.上述代码中数据的单位:
上述代码中,获取控件的宽、高的单位都是px(demo中有测试代码);
2.部分方法获取数据为0:
获取控件的宽、高时,有mTvTest.getWidth()和mTvTest.getHeight()的api,但实际测试后你会发现,它们的数据可能为0。既然谷歌都提供了相应的api,但测试后发现却为0,这不是坑爹吗?
其实,出现这种现象的原因,是因为我们获取控件需要在onCreate()或者onResume()方法中去获取,但view的measure过程与Activity的生命周期不是同步执行的,因此无法保证在onCreat(),onResume(),onStart()时这个view是否已经测量完毕,如果没有测量完毕,得到的结果就是0。所以比较保险的方法是上述代码中的做法。当然,正确获取的方式有多种,网上也有很多。比如这篇(点击即可跳转)。
3.深入学习:
如果你想更进一步的理解控件测量的知识,可以看我写的这篇博客(点击即可跳转)。
好了,android与屏幕有关的知识点(一)到这里就结束了.支持的、吐槽的、有疑问的、以及打酱油的路过朋友尽管留言吧 v 感兴趣的朋友可以继续阅读 Android与屏幕有关的知识点(二)。
对于屏幕的适配问题,其实很多Android前辈作了大量的开源工作,他们的知识的理解远远比我深、更全,有兴趣的可以看一下:
点我下载
github链接: 点我跳转