导语
为了用户体验更好,优美的界面必不可少,所以,绘图就很重要了。
主要内容
- Android屏幕相关知识
- Android 2D绘图基础
- Android XML绘图
- Android绘图简单技巧
具体内容
屏幕的尺寸信息
主要还是因为Android的屏幕确实五花八门,所以在一定程度上的适配问题也是很捉急的,所以我们要对这块屏幕充分的认识。
屏幕参数
一块屏幕通常具备以下的几个参数:
- 屏幕大小:指屏幕对角线的长度,通常用寸来表示,例如4.7寸,5.5寸。
- 分辨率:分辨率是指实际屏幕的像素点个数,例如720X1280就是指屏幕的分辨率,宽有720个像素点,高有1280个像素点。
- PPI:每英寸像素又称为DPI,他是由对角线的的像素点数除以屏幕的大小所得,通常有400PPI就已经很6了。
系统屏幕密度
每个厂商的安卓手机具有不同的大小尺寸和像素密度的屏幕,安卓系统如果要精确到每种DPI的屏幕,基本上是不可能的,因此系统定义了几个标准的DPI。
密度 | 密度值 | 分辨率 |
---|---|---|
ldpi | 120 | 240×320 |
mdpi | 160 | 320×480 |
hdpi | 240 | 480×800 |
xhdpi | 320 | 720×1280 |
xxhdpi | 480 | 1080×1920 |
独立像素密度dp
这是由于各种屏幕密度的不同,导致同样像素大小的长度,在不同密度的屏幕上显示长度不同,因此相同长度的屏幕,高密度的屏幕包含更多的像素点,在安卓系统中使用mdpi密度值为160的屏幕作为标准,在这个屏幕上,1px = 1dp,其他屏幕则可以通过比例进行换算,例如同样是100dp的长度,mdpi中为100px,而在hdpi中为150,我们也可以得出在各个密度值中的换算公式,在mdpi中 1dp = 1px,在hdpi中,1dp = 1.5px,在xhdpi中,1dp = 2px,在xxhdpi中1dp = 3px,由此可见,我们换算比例即: l:m:h:xh:xxh = 3:4:6:8:12。
单位换算
在程序中,我们可以非常方便地对一些单位的换算,下面的代码给出了一种换算的方法我们可以把这些代码作为工具类保存在项目中。
package com.lgl.playview;
import android.content.Context;
/**
* dp,sp转换成px的工具类
* Created by lgl on 16/3/23.
*/
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());
}
2D绘图基础
系统通过提供的Canvas对象提供绘图方法。它提供了各种绘制图像的API,如drawPoint(点)、drawLine(线)、drawRect(矩形)、drawVertices(多边形)、drawArc(弧)、darwCircle(圆),等等。Paint作为一个非常重要的元素,功能也是很强大的,这里简单地列举一些它的属性和对应的功能。
- setAntiAlias():设置画笔的锯齿效果。
- setColor():设置画笔的颜色。
- setARGB():设置画笔的A、R、G、B值。
- setAlpha():设置画笔的Alpha值。
- setTextSize():设置字体的尺寸。
- setStyle():设置画笔的风格(空心或实心)。
- setStrokeWidth():设置空心边框的宽度。
- getColor():获取画笔的颜色。
设置Paint的Style可以画出空心或者实心的矩形:
paint.setStyle(Paint.Style.STROKE); // 空心效果
paint.setStyle(Paint.Style.FILL); // 实心效果
系统通过提供的Canvas对象来提供绘图方法:
- canvas.drawPoint(x, y, paint):绘制点。
- canvas.drawLine(startX, startY ,endX, endY, paint):绘制直线。
- canvas.drawLines(new float[]{startX1, startY1, endX1, endY1,……,startXn, startYn, endXn, endYn}, paint):绘制多条直线。
- canvas.drawRect(left, top, right, bottom, paint):绘制矩形。
- canvas.drawRoundRect(left, top, right, bottom, radiusX, radiusY, paint):绘制圆角矩形。
- canvas.drawCircle(circleX, circleY, radius, paint):绘制圆。
- canvas.drawOval(left, top, right, bottom, paint):绘制椭圆。
- canvas.drawText(text, startX, startY, paint):绘制文本。
- canvas.drawPosText(text, new float[]{X1,Y1,X2,Y2,……Xn,Yn}, paint):在指定位置绘制文本。
- Path path = new Path();
path.moveTo(50, 50);
path.lineTo(100, 100);
path.lineTo(100, 300);
path.lineTo(300, 50);
canvas.drawPath(path, paint):绘制路径。
- 绘制扇形:
paint.setStyle(Paint.Style.STROKE);
drawArc(left, top, right,bottom, startAngle, sweepAngle, true, paint); - 绘制弧形:
paint.setStyle(Paint.Style.STROKE);
drawArc(left, top, right,bottom, startAngle, sweepAngle, false, paint); - 绘制实心扇形:
paint.setStyle(Paint.Style.FILL);
drawArc(left, top, right,bottom, startAngle, sweepAngle, true, paint); - 绘制实心弧形:
paint.setStyle(Paint.Style.FILL);
drawArc(left, top, right,bottom, startAngle, sweepAngle, false, paint);
Android XML绘图
XML在Android系统中不仅是Java中的一个布局文件、配置列表。在Android开发者的手上,它可以变成一张画、一幅图。Android的开发者给XML提供了几个强大的技能来帮助实现这一功能。
bitmap
通过这样在XML中使用Bitmap就可以将图片直接转换成了Bitmap在程序中使用。
<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@mipmap/ic_launcher">
</bitmap>
通过这样引用图片就可以将图片直接转化成Bitmap让我们在程序中使用。
Shape
通过Shape可以在XML中绘制各种形状:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<!--默认是rectangle-->
<!--当shape= rectangle的时候使用-->
<corners
android:bottomLeftRadius="1dp"
android:bottomRightRadius="1dp"
android:radius="1dp"
android:topLeftRadius="1dp"
android:topRightRadius="1dp" />
<!--半径,会被后面的单个半径属性覆盖,默认是1dp-->
<!--渐变-->
<gradient
android:angle="1dp"
android:centerColor="@color/colorAccent"
android:centerX="1dp"
android:centerY="1dp"
android:gradientRadius="1dp"
android:startColor="@color/colorAccent"
android:type="linear"
android:useLevel="true" />
<!--内间距-->
<padding
android:bottom="1dp"
android:left="1dp"
android:right="1dp"
android:top="1dp" />
<!--大小,主要用于imageview用于scaletype-->
<size
android:width="1dp"
android:height="1dp" />
<!--填充颜色-->
<solid android:color="@color/colorAccent" />
<!--指定边框-->
<stroke
android:width="1dp"
android:color="@color/colorAccent" />
<!--虚线宽度-->
android:dashWidth= "1dp"
<!--虚线间隔宽度-->
android:dashGap= "1dp"
</shape>
shape可以说是xml绘图的精华所在,而且功能十分的强大,无论是扁平化,拟物化还是渐变,都是十分的OK,我们现在来做一个阴影的效果。
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="45"
android:endColor="#805FBBFF"
android:startColor="#FF5DA2FF" />
<padding
android:bottom="7dp"
android:left="7dp"
android:right="7dp"
android:top="7dp" />
<corners android:radius="8dp" />
</shape>
![Uploading 20160327205743972_658299.gif . . .]
Layer
Layer是在PhotoShop中是非常常用的功能,在Android中,我们同样可以实现图层的效果。
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!--图片1-->
<item android:drawable="@mipmap/ic_launcher"/>
<!--图片2-->
<item
android:bottom="10dp"
android:top="10dp"
android:right="10dp"
android:left="10dp"
android:drawable="@mipmap/ic_launcher"
/>
</layer-list>
Selector
Selector的作用是帮助开发者实现静态View的反馈,通过设置不同的属性呈现不同的效果。
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 默认时候的背景-->
<item android:drawable="@mipmap/ic_launcher" />
<!-- 没有焦点时候的背景-->
<item android:drawable="@mipmap/ic_launcher" android:state_window_focused="false" />
<!-- 非触摸模式下获得焦点并点击时的背景图片-->
<item android:drawable="@mipmap/ic_launcher" android:state_pressed="true" android:state_window_focused="true" />
<!-- 触摸模式下获得焦点并点击时的背景图片-->
<item android:drawable="@mipmap/ic_launcher" android:state_focused="false" android:state_pressed="true" />
<!--选中时的图片背景-->
<item android:drawable="@mipmap/ic_launcher" android:state_selected="true" />
<!--获得焦点时的图片背景-->
<item android:drawable="@mipmap/ic_launcher" android:state_focused="true" />
</selector>
这一方法可以帮助开发者迅速制作View的反馈,通过配置不同的触发事件,selector会自动选中不同的图片,特别是自定义button的时候,而我们不再使用原生单调的背景,而是使用selector特别制作的背景,就能完美实现触摸反馈了。
通常情况下,上面提到的这些方法都可以共同实现,下面这个例子就展示了在一个selector中使用shape作为他的item的例子,实现一个具体点击反馈效果的,圆角矩形的selector,代码如下。
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<!--填充颜色-->
<solid android:color="#33444444" />
<!--设置按钮的四个角为弧形-->
<corners android:radius="5dp" />
<!--间距-->
<padding android:bottom="10dp" android:left="10dp" android:right="10dp" android:top="10dp" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<!--填充颜色-->
<solid android:color="#FFFFFF" />
<!--设置按钮的四个角为弧形-->
<corners android:radius="5dp" />
<!--间距-->
<padding android:bottom="10dp" android:left="10dp" android:right="10dp" android:top="10dp" />
</shape>
</item>
</selector>
效果图。
Android绘图技巧
在学完Android的基本绘图之后我们来讲解一下常用的绘图技巧。
Canvas
Canvas作为绘制图形的直接对象,提供了一下几个非常有用的方法:
- Canvas.save()。
- Canvas.restore()。
- Canvas.translate()。
- Canvas.roate()。
首先,我们来看一下前面两个方法:
在讲解这两个方法之前,首先来了解一下Android绘图的坐标体系,这个其实这个前面已经讲了,这里不赘述,而Canvas.save()这个方法,从字面上的意思可以理解为保存画布,他的作用就是讲之前的图像保存起来,让后续的操作能像在新的画布一样操作,这跟PS的图层基本差不多。
而Canvas.restore()这个方法,则可以理解为合并图层,就是讲之前保存下来的东西合并。
而后面两个方法尼?从字母上理解画布平移或者旋转,但是把他理解为坐标旋转更加形象,前面说了,我们绘制的时候默认坐标点事左上角的起始点,那么我们调用translate(x,y)之后,则将原点(0,0)移动到(x,y)之后的所有绘图都是在这一点上执行的,这里可能说的不够详细,最典型的例子是画一个表盘了,那我们这里就演示一下画一个表盘。
我们先来分析一下这个表盘有什么?我们可以将他分解:
- 仪表盘——外面的大圆盘。
- 刻度线——包含四个长的刻度线和其他短的刻度线。
- 刻度值——包含长刻度线对应的大的刻度尺和其他小的刻度尺。
- 指针——中间的指针,一粗一细两根。
相信如果现实中叫你去画一个仪表盘的话,你应该也会这样的步骤去画,实际上Android上的绘图远比现实中的绘制十分的相似,与PS的绘图更家相似,当然,我们在绘制一个复杂的图形之后,不妨先把思路请清除了。
这个示例中,我们第一步,先画表盘,现在画个圆应该很轻松了。关键在于确定圆心和半径,这里直接居中吧。
// 画外圆
Paint paintCircle = new Paint();
paintCircle.setAntiAlias(true);
paintCircle.setStyle(Paint.Style.STROKE);
paintCircle.setStrokeWidth(5);
canvas.drawCircle(mWidth / 2, mHeight / 2, mWidth / 2, paintCircle);
下面,我们来画刻度尺,这个也很简单,一条线而已,只要确定两个端点的位置就可以,第一根线还是比较容易确定的,那后面的我们怎么去确定尼?那些斜着的我们可以用三角函数去实现计算,但是这其实就是一个很简单的画图,三角函数的运算也要这么多,我们不经要思考,该怎么去简化他尼?其实Google已经为我们想好了。
我们治国与会觉得这个不好画,主要是这个角度,那么如果我们将画布以中心为原点旋转到需要的角度尼?每当画好一根线,我们就旋转多少级角度,但是下一次划线的时候依然是第一次的坐标,但是实际上我们把画布重新还原到旋转钱的坐标了,所有的刻度线就已经画好了,通过旋转画布——实际上是旋转了画图的坐标轴,这就避免了万恶的三角函数了,通过这样一种相对论式的变换,间接简化了绘图,这时再去绘制这些刻度,是不是要简单了,只需要区别整点和非整点的刻度了。
// 画刻度
Paint paintDegree = new Paint();
paintDegree.setStrokeWidth(3);
for (int i = 0; i < 24; i++) {
// 区别整点和非整点
if (i == 0 || i == 6 || i == 12 || i == 18) {
paintDegree.setStrokeWidth(5);
paintDegree.setTextSize(30);
canvas.drawLine(mWidth / 2, mHeight / 2 - mWidth / 2, mWidth,
mHeight / 2 - mWidth / 2 + 60, paintDegree);
String degree = String.valueOf(i);
canvas.drawText(degree,
mWidth / 2 - paintDegree.measureText(degree) / 2,
mHeight / 2 - mWidth / 2 + 90, paintDegree);
} else {
paintDegree.setStrokeWidth(3);
paintDegree.setTextSize(15);
canvas.drawLine(mWidth / 2, mHeight / 2 - mWidth / 2, mWidth,
mHeight / 2 - mWidth / 2 + 30, paintDegree);
String degree = String.valueOf(i);
canvas.drawText(degree,
mWidth / 2 - paintDegree.measureText(degree) / 2,
mHeight / 2 - mWidth / 2 + 60, paintDegree);
}
// 通过旋转画布简化坐标运算
canvas.rotate(15, mWidth / 2, mHeight / 2);
}
紧接着,我们就可以来绘制两根针了,我们可以这样来绘制。
// 画指针
Paint paintHour = new Paint();
paintHour.setStrokeWidth(20);
Paint paintMinute = new Paint();
paintMinute.setStrokeWidth(10);
canvas.save();
canvas.translate(mWidth / 2, mHeight / 2);
canvas.drawLine(0, 0, 100, 100, paintHour);
canvas.drawLine(0, 0, 100, 200, paintMinute);
canvas.restore();
这样运行的效果就和最上面的效果图一样了,这里贴上完整的代码。
package com.lgl.dial;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import android.view.WindowManager;
public class DialView extends View {
// 宽高
private int mWidth;
private int mHeight;
// 构造方法
public DialView(Context context, AttributeSet attrs) {
super(context, attrs);
// 获取屏幕的宽高
WindowManager wm = (WindowManager) getContext().getSystemService(
Context.WINDOW_SERVICE);
mWidth = wm.getDefaultDisplay().getWidth();
mHeight = wm.getDefaultDisplay().getHeight();
}
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
// 画外圆
Paint paintCircle = new Paint();
paintCircle.setAntiAlias(true);
paintCircle.setStyle(Paint.Style.STROKE);
paintCircle.setStrokeWidth(5);
canvas.drawCircle(mWidth / 2, mHeight / 2, mWidth / 2, paintCircle);
// 画刻度
Paint paintDegree = new Paint();
paintDegree.setStrokeWidth(3);
for (int i = 0; i < 24; i++) {
// 区别整点和非整点
if (i == 0 || i == 6 || i == 12 || i == 18) {
paintDegree.setStrokeWidth(5);
paintDegree.setTextSize(30);
canvas.drawLine(mWidth / 2, mHeight / 2 - mWidth / 2,
mWidth / 2, mHeight / 2 - mWidth / 2 + 60, paintDegree);
String degree = String.valueOf(i);
canvas.drawText(degree,
mWidth / 2 - paintDegree.measureText(degree) / 2,
mHeight / 2 - mWidth / 2 + 90, paintDegree);
} else {
paintDegree.setStrokeWidth(3);
paintDegree.setTextSize(15);
canvas.drawLine(mWidth / 2, mHeight / 2 - mWidth / 2,
mWidth / 2, mHeight / 2 - mWidth / 2 + 30, paintDegree);
String degree = String.valueOf(i);
canvas.drawText(degree,
mWidth / 2 - paintDegree.measureText(degree) / 2,
mHeight / 2 - mWidth / 2 + 60, paintDegree);
}
// 通过旋转画布简化坐标运算
canvas.rotate(15, mWidth / 2, mHeight / 2);
}
// 画指针
Paint paintHour = new Paint();
paintHour.setStrokeWidth(20);
Paint paintMinute = new Paint();
paintMinute.setStrokeWidth(10);
canvas.save();
canvas.translate(mWidth / 2, mHeight / 2);
canvas.drawLine(0, 0, 100, 100, paintHour);
canvas.drawLine(0, 0, 100, 200, paintMinute);
canvas.restore();
}
}
Layer图层
Android中的绘图API,很大程度上都来自绘图的API,特别是借鉴了很多PS的原理,比如图层的概念,相信看过图层的也都知道是个什么样的,我们画个图来分析一下。
Android通过saveLayer()方法,saveLayerAlpha()将一个图层入栈,使用restore()方法,restoreToCount()方法将一个图层出栈,入栈的时候,后面的所有才做都是发生在这个图层上的,而出栈的时候,则会把图层绘制在上层Canvas上,我们仿照API Demo来。
@Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
canvas.drawColor(Color.WHITE);
mPaint.setColor(Color.BLUE);
canvas.drawCircle(150, 150, 100, mPaint);
canvas.saveLayerAlpha(0, 0,400,400,127,LAYER_TYPE_NONE);
mPaint.setColor(Color.RED);
canvas.drawCircle(200, 200, 100, mPaint);
canvas.restore();
}
当绘制两个相交的圆时,就是图层。
接下来将图层后面的透明度设置成0-255不同值 。
我们分别演示127、255、0三个。