導語
為了用戶體驗更好,優(yōu)美的界面必不可少,所以,繪圖就很重要了。
主要內(nèi)容
- Android屏幕相關(guān)知識
- Android 2D繪圖基礎(chǔ)
- Android XML繪圖
- Android繪圖簡單技巧
具體內(nèi)容
屏幕的尺寸信息
主要還是因為Android的屏幕確實五花八門,所以在一定程度上的適配問題也是很捉急的,所以我們要對這塊屏幕充分的認識。
屏幕參數(shù)
一塊屏幕通常具備以下的幾個參數(shù):
- 屏幕大小:指屏幕對角線的長度,通常用寸來表示,例如4.7寸,5.5寸。
- 分辨率:分辨率是指實際屏幕的像素點個數(shù),例如720X1280就是指屏幕的分辨率,寬有720個像素點,高有1280個像素點。
- PPI:每英寸像素又稱為DPI,他是由對角線的的像素點數(shù)除以屏幕的大小所得,通常有400PPI就已經(jīng)很6了。
系統(tǒng)屏幕密度
每個廠商的安卓手機具有不同的大小尺寸和像素密度的屏幕,安卓系統(tǒng)如果要精確到每種DPI的屏幕,基本上是不可能的,因此系統(tǒng)定義了幾個標準的DPI。
密度 | 密度值 | 分辨率 |
---|---|---|
ldpi | 120 | 240×320 |
mdpi | 160 | 320×480 |
hdpi | 240 | 480×800 |
xhdpi | 320 | 720×1280 |
xxhdpi | 480 | 1080×1920 |
獨立像素密度dp
這是由于各種屏幕密度的不同,導致同樣像素大小的長度,在不同密度的屏幕上顯示長度不同,因此相同長度的屏幕,高密度的屏幕包含更多的像素點,在安卓系統(tǒng)中使用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轉(zhuǎn)換成px的工具類
* Created by lgl on 16/3/23.
*/
public class DisplayUtils {
/**
* 將px值轉(zhuǎn)換成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轉(zhuǎn)化成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轉(zhuǎn)化成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轉(zhuǎn)化成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就是前面所說的換算比例,這里使用的是公式換算方法進行轉(zhuǎn)換,同時系統(tǒng)也提供了TypedValue幫助我們轉(zhuǎn)換。
/**
* 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繪圖基礎(chǔ)
系統(tǒng)通過提供的Canvas對象提供繪圖方法。它提供了各種繪制圖像的API,如drawPoint(點)、drawLine(線)、drawRect(矩形)、drawVertices(多邊形)、drawArc(弧)、darwCircle(圓),等等。Paint作為一個非常重要的元素,功能也是很強大的,這里簡單地列舉一些它的屬性和對應的功能。
- setAntiAlias():設(shè)置畫筆的鋸齒效果。
- setColor():設(shè)置畫筆的顏色。
- setARGB():設(shè)置畫筆的A、R、G、B值。
- setAlpha():設(shè)置畫筆的Alpha值。
- setTextSize():設(shè)置字體的尺寸。
- setStyle():設(shè)置畫筆的風格(空心或?qū)嵭模?/li>
- setStrokeWidth():設(shè)置空心邊框的寬度。
- getColor():獲取畫筆的顏色。
設(shè)置Paint的Style可以畫出空心或者實心的矩形:
paint.setStyle(Paint.Style.STROKE); // 空心效果
paint.setStyle(Paint.Style.FILL); // 實心效果
系統(tǒng)通過提供的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系統(tǒng)中不僅是Java中的一個布局文件、配置列表。在Android開發(fā)者的手上,它可以變成一張畫、一幅圖。Android的開發(fā)者給XML提供了幾個強大的技能來幫助實現(xiàn)這一功能。
bitmap
通過這樣在XML中使用Bitmap就可以將圖片直接轉(zhuǎn)換成了Bitmap在程序中使用。
<?xml version="1.0" encoding="utf-8"?>
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
android:src="@mipmap/ic_launcher">
</bitmap>
通過這樣引用圖片就可以將圖片直接轉(zhuǎn)化成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" />
<!--內(nèi)間距-->
<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,我們現(xiàn)在來做一個陰影的效果。
<?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中,我們同樣可以實現(xiàn)圖層的效果。
<?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的作用是幫助開發(fā)者實現(xiàn)靜態(tài)View的反饋,通過設(shè)置不同的屬性呈現(xiàn)不同的效果。
<?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>
這一方法可以幫助開發(fā)者迅速制作View的反饋,通過配置不同的觸發(fā)事件,selector會自動選中不同的圖片,特別是自定義button的時候,而我們不再使用原生單調(diào)的背景,而是使用selector特別制作的背景,就能完美實現(xiàn)觸摸反饋了。
通常情況下,上面提到的這些方法都可以共同實現(xiàn),下面這個例子就展示了在一個selector中使用shape作為他的item的例子,實現(xiàn)一個具體點擊反饋效果的,圓角矩形的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" />
<!--設(shè)置按鈕的四個角為弧形-->
<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" />
<!--設(shè)置按鈕的四個角為弧形-->
<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繪圖的坐標體系,這個其實這個前面已經(jīng)講了,這里不贅述,而Canvas.save()這個方法,從字面上的意思可以理解為保存畫布,他的作用就是講之前的圖像保存起來,讓后續(xù)的操作能像在新的畫布一樣操作,這跟PS的圖層基本差不多。
而Canvas.restore()這個方法,則可以理解為合并圖層,就是講之前保存下來的東西合并。
而后面兩個方法尼?從字母上理解畫布平移或者旋轉(zhuǎn),但是把他理解為坐標旋轉(zhuǎn)更加形象,前面說了,我們繪制的時候默認坐標點事左上角的起始點,那么我們調(diào)用translate(x,y)之后,則將原點(0,0)移動到(x,y)之后的所有繪圖都是在這一點上執(zhí)行的,這里可能說的不夠詳細,最典型的例子是畫一個表盤了,那我們這里就演示一下畫一個表盤。
我們先來分析一下這個表盤有什么?我們可以將他分解:
- 儀表盤——外面的大圓盤。
- 刻度線——包含四個長的刻度線和其他短的刻度線。
- 刻度值——包含長刻度線對應的大的刻度尺和其他小的刻度尺。
- 指針——中間的指針,一粗一細兩根。
相信如果現(xiàn)實中叫你去畫一個儀表盤的話,你應該也會這樣的步驟去畫,實際上Android上的繪圖遠比現(xiàn)實中的繪制十分的相似,與PS的繪圖更家相似,當然,我們在繪制一個復雜的圖形之后,不妨先把思路請清除了。
這個示例中,我們第一步,先畫表盤,現(xiàn)在畫個圓應該很輕松了。關(guān)鍵在于確定圓心和半徑,這里直接居中吧。
// 畫外圓
Paint paintCircle = new Paint();
paintCircle.setAntiAlias(true);
paintCircle.setStyle(Paint.Style.STROKE);
paintCircle.setStrokeWidth(5);
canvas.drawCircle(mWidth / 2, mHeight / 2, mWidth / 2, paintCircle);
下面,我們來畫刻度尺,這個也很簡單,一條線而已,只要確定兩個端點的位置就可以,第一根線還是比較容易確定的,那后面的我們怎么去確定尼?那些斜著的我們可以用三角函數(shù)去實現(xiàn)計算,但是這其實就是一個很簡單的畫圖,三角函數(shù)的運算也要這么多,我們不經(jīng)要思考,該怎么去簡化他尼?其實Google已經(jīng)為我們想好了。
我們治國與會覺得這個不好畫,主要是這個角度,那么如果我們將畫布以中心為原點旋轉(zhuǎn)到需要的角度尼?每當畫好一根線,我們就旋轉(zhuǎn)多少級角度,但是下一次劃線的時候依然是第一次的坐標,但是實際上我們把畫布重新還原到旋轉(zhuǎn)錢的坐標了,所有的刻度線就已經(jīng)畫好了,通過旋轉(zhuǎn)畫布——實際上是旋轉(zhuǎn)了畫圖的坐標軸,這就避免了萬惡的三角函數(shù)了,通過這樣一種相對論式的變換,間接簡化了繪圖,這時再去繪制這些刻度,是不是要簡單了,只需要區(qū)別整點和非整點的刻度了。
// 畫刻度
Paint paintDegree = new Paint();
paintDegree.setStrokeWidth(3);
for (int i = 0; i < 24; i++) {
// 區(qū)別整點和非整點
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);
}
// 通過旋轉(zhuǎn)畫布簡化坐標運算
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;
// 構(gòu)造方法
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++) {
// 區(qū)別整點和非整點
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);
}
// 通過旋轉(zhuǎn)畫布簡化坐標運算
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()方法將一個圖層出棧,入棧的時候,后面的所有才做都是發(fā)生在這個圖層上的,而出棧的時候,則會把圖層繪制在上層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();
}
當繪制兩個相交的圓時,就是圖層。
接下來將圖層后面的透明度設(shè)置成0-255不同值 。
我們分別演示127、255、0三個。