關於做者html
郭孝星,程序員,吉他手,主要從事Android平臺基礎架構方面的工做,歡迎交流技術方面的問題,能夠去個人Github提issue或者發郵件至guoxiaoxingse@163.com與我交流。java
文章目錄android
第一次閱覽本系列文章,請參見導讀,更多文章請參見文章目錄。git
文章源碼程序員
本文還提供了三個綜合性的完整實例來輔助理解。github
第一次閱覽本系列文章,請參見導讀,更多文章請參見文章目錄。算法
本篇文章咱們來分析View繪製方面的實踐。canvas
一個簡單的自定義View數組
public class DrawView extends View {
Paint paint = new Paint();
public DrawView(Context context) {
super(context);
}
public DrawView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public DrawView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setColor(Color.BLACK);
canvas.drawCircle(150, 150, 150, paint);
}
}複製代碼
它在屏幕上繪製了一個圓形,如圖:緩存
在處理繪製的時候有如下幾個關鍵點:
咱們分別來看看這個關鍵的角色。
咱們討論的第一個問題就是View/ViewGroup的繪製順序問題,繪製在View.draw()方法裏調用的,具體的執行順序是:
咱們先從個小例子開始。
咱們若是繼承View來實現自定義View。View類的onDraw()是空實現,因此咱們的繪製代碼寫在super.onDraw(canvas)的前面或者後面都沒有關係,以下所示:
public class DrawView extends View {
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//繪製代碼,寫在super.onDraw(canvas)先後都可
}
}複製代碼
可是若是咱們繼承特定的控件,例如TextView。咱們就須要去考慮TextView的繪製邏輯。
public class DrawView extends TextView {
@Override
protected void onDraw(Canvas canvas) {
//寫在前面,DrawView的繪製會先於TextView的繪製,TextView繪製的內容能夠會覆蓋DrawView
super.onDraw(canvas);
//寫在後面,DrawView的繪製會晚於TextView的繪製,DrawView繪製的內容能夠會覆蓋TextView
}
}複製代碼
具體怎麼作取決於你實際的需求,例如你若是想給TextView加個背景,就寫在super.onDraw(canvas)前面,想給TextView前面加些點綴,就
寫在super.onDraw(canvas)後面。
咱們來寫個例子理解下。
舉例
public class LabelImageView extends AppCompatImageView {
/** * 梯形距離左上角的長度 */
private static final int LABEL_LENGTH = 100;
/** * 梯形斜邊的長度 */
private static final int LABEL_HYPOTENUSE_LENGTH = 100;
private Paint textPaint;
private Paint backgroundPaint;
private Path pathText;
private Path pathBackground;
public LabelImageView(Context context) {
super(context);
init();
}
public LabelImageView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public LabelImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//計算路徑
calculatePath(getMeasuredWidth(), getMeasuredHeight());
canvas.drawPath(pathBackground, backgroundPaint);
canvas.drawTextOnPath("Hot", pathText, 100, -20, textPaint);
}
@Override
public void onDrawForeground(Canvas canvas) {
super.onDrawForeground(canvas);
}
/** * 計算路徑 x1 x2 * ................................ distance(標籤離右上角的垂直距離) * . . . . * . . .. y1 * . . . * . . . * . . y2 height(標籤垂直高度) * . . * ................................ */
private void calculatePath(int measuredWidth, int measuredHeight) {
int top = 185;
int right = measuredWidth;
float x1 = right - LABEL_LENGTH - LABEL_HYPOTENUSE_LENGTH;
float x2 = right - LABEL_HYPOTENUSE_LENGTH;
float y1 = top + LABEL_LENGTH;
float y2 = top + LABEL_LENGTH + LABEL_HYPOTENUSE_LENGTH;
pathText.reset();
pathText.moveTo(x1, top);
pathText.lineTo(right, y2);
pathText.close();
pathBackground.reset();
pathBackground.moveTo(x1, top);
pathBackground.lineTo(x2, top);
pathBackground.lineTo(right, y1);
pathBackground.lineTo(right, y2);
pathBackground.close();
}
private void init() {
pathText = new Path();
pathBackground = new Path();
textPaint = new Paint();
textPaint.setTextSize(50);
textPaint.setFakeBoldText(true);
textPaint.setColor(Color.WHITE);
backgroundPaint = new Paint();
backgroundPaint.setColor(Color.RED);
backgroundPaint.setStyle(Paint.Style.FILL);
}
}複製代碼
因此你能夠看到,當咱們繼承了一個View,根據需求的不一樣能夠選擇性重寫咱們須要的方法,在super前插入代碼和在super後插入代碼,效果是不同的。
Paint:顧名思義,畫筆,經過Paint能夠對繪製行爲進行控制。
Paint有三種構造方法
public class Paint {
//空的構造方法
public Paint() {
this(0);
}
//傳入flags來構造Paint,flags用來控制Paint的行爲,例如:抗鋸齒等
public Paint(int flags) {
mNativePaint = nInit();
NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, mNativePaint);
setFlags(flags | HIDDEN_DEFAULT_PAINT_FLAGS);
// TODO: Turning off hinting has undesirable side effects, we need to
// revisit hinting once we add support for subpixel positioning
// setHinting(DisplayMetrics.DENSITY_DEVICE >= DisplayMetrics.DENSITY_TV
// ? HINTING_OFF : HINTING_ON);
mCompatScaling = mInvCompatScaling = 1;
setTextLocales(LocaleList.getAdjustedDefault());
}
//傳入另一個Paint來構造新的Paint
public Paint(Paint paint) {
mNativePaint = nInitWithPaint(paint.getNativeInstance());
NoImagePreloadHolder.sRegistry.registerNativeAllocation(this, mNativePaint);
setClassVariablesFrom(paint);
}
}複製代碼
在Paint類中,處理顏色主要有三個方法。
着色器是圖像領域的一個通用概念,它提供的是一套着色規則。
public Shader setShader(Shader shader)複製代碼
着色器具體由Shader的子類實現:
LinearGradient - 線性漸變
public LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1, TileMode tile)複製代碼
舉例
//線性漸變
Shader shader1 = new LinearGradient(0, 100, 200, 100, Color.RED, Color.BLUE, Shader.TileMode.CLAMP);
paint1.setShader(shader1);
Shader shader2 = new LinearGradient(0, 600, 200, 600, Color.RED, Color.BLUE, Shader.TileMode.MIRROR);
paint2.setShader(shader2);
Shader shader3 = new LinearGradient(0, 1100, 200, 1100, Color.RED, Color.BLUE, Shader.TileMode.REPEAT);
paint3.setShader(shader3);
canvas.drawRect(0, 100, 1000, 500, paint1);
canvas.drawRect(0, 600, 1000, 1000, paint2);
canvas.drawRect(0, 1100, 1000, 1500, paint3);複製代碼
SweepGradient - 輻射漸變
public RadialGradient(float centerX, float centerY, float radius, int centerColor, int edgeColor, @NonNull TileMode tileMode)複製代碼
舉例
//輻射漸變
Shader shader1 = new RadialGradient(0, 100, 200, Color.RED, Color.BLUE, Shader.TileMode.CLAMP);
paint1.setShader(shader1);
Shader shader2 = new RadialGradient(0, 600, 200, Color.RED, Color.BLUE, Shader.TileMode.MIRROR);
paint2.setShader(shader2);
Shader shader3 = new RadialGradient(0, 1100, 200, Color.RED, Color.BLUE, Shader.TileMode.REPEAT);
paint3.setShader(shader3);
canvas.drawRect(0, 100, 1000, 500, paint1);
canvas.drawRect(0, 600, 1000, 1000, paint2);複製代碼
BitmapShader - 位圖着色
使用位圖的像素來填充圖形或者文字。
public BitmapShader(@NonNull Bitmap bitmap, TileMode tileX, TileMode tileY)複製代碼
舉例
BitmapShader是一個頗有用的類,能夠利用該類作各類各樣的圖片裁剪。
//位圖着色
Shader shader1 = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
paint1.setShader(shader1);
//繪製圓形
canvas.drawCircle(500, 500, 300, paint1);複製代碼
ComposeShader - 組合Shader
ComposeShader能夠將連個Shader組合在一塊兒。
public ComposeShader(Shader shaderA, Shader shaderB, PorterDuff.Mode mode)複製代碼
PorterDuff.Mode用來指定兩個Shader疊加時顏色的繪製策略,它有不少種策略,也就是以一種怎樣的模式來與原圖像進行合成,具體以下:
藍色矩形爲原圖像,紅色圓形爲目標圖像。
更多細節能夠參見PorterDuff.Mode官方文檔。
顏色過濾器能夠將顏色按照必定的規則輸出,常見於各類濾鏡效果。
public ColorFilter setColorFilter(ColorFilter filter)複製代碼
咱們一般使用的是ColorFilter的三個子類:
LightingColorFilter - 模擬光照效果
public LightingColorFilter(int mul, int add)複製代碼
mul 和 add 都是和顏色值格式相同的 int 值,其中 mul 用來和目標像素相乘,add 用來和目標像素相加。
舉例
//顏色過濾器
ColorFilter colorFilter1 = new LightingColorFilter(Color.RED, Color.BLUE);
paint2.setColorFilter(colorFilter1);
canvas.drawBitmap(bitmapTimo, null, rect1, paint1);
canvas.drawBitmap(bitmapTimo, null, rect2, paint2);複製代碼
PorterDuffColorFilter - 模擬顏色混合效果
public PorterDuffColorFilter(@ColorInt int color, @NonNull PorterDuff.Mode mode)複製代碼
PorterDuffColorFilter指定一種顏色和PorterDuff.Mode來與源圖像就行合成,也就是以一種怎樣的模式來與原圖像進行合成,咱們在上面已經講過這個內容。
舉例
//咱們在使用Xfermode的時候也是使用它的子類PorterDuffXfermode
Xfermode xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_IN);
canvas.drawBitmap(rectBitmap, 0, 0, paint); // 畫方
paint.setXfermode(xfermode); // 設置 Xfermode
canvas.drawBitmap(circleBitmap, 0, 0, paint); // 畫圓
paint.setXfermode(null); // 用完及時清除 Xfermode複製代碼
ColorMatrixColorFilter - 顏色矩陣過濾
ColorMatrixColorFilter使用一個顏色矩陣ColorMatrix來對象圖像進行處理。
public ColorMatrixColorFilter(ColorMatrix matrix)複製代碼
ColorMatrix是一個4x5的矩陣
[ a, b, c, d, e,
f, g, h, i, j,
k, l, m, n, o,
p, q, r, s, t ]複製代碼
經過計算,ColorMatrix能夠對要繪製的像素進行轉換,以下:
R’ = a*R + b*G + c*B + d*A + e;
G’ = f*R + g*G + h*B + i*A + j;
B’ = k*R + l*G + m*B + n*A + o;
A’ = p*R + q*G + r*B + s*A + t;複製代碼
利用ColorMatrixColorFilter(能夠實現不少炫酷的濾鏡效果。
Paint.setXfermode(Xfermode xfermode)方法,它也是一種混合圖像的方法。
Xfermode 指的是你要繪製的內容和 Canvas 的目標位置的內容應該怎樣結合計算出最終的顏色。但通俗地說,其實就是要你以繪製的內容做爲源圖像,以View中已有的內
容做爲目標圖像,選取一個PorterDuff.Mode做爲繪製內容的顏色處理方案。
小結
關於PorterDuff.Mode,咱們已經提到
這三種以不一樣的方式來使用PorterDuff.Mode,可是原理都是同樣的。
Paint裏有大量方法來設置文字的繪製屬性,事實上文字在Android底層是被當作圖片來處理的。
設置抗鋸齒,默認關閉,用來是圖像的繪製更加圓潤。咱們還能夠在初始化的時候設置Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);。
設置填充風格,
若是是劃線模式,咱們針對線條還能夠有多種設置。
setStrokeWidth(float width) - 設置線條粗細
setStrokeCap(Paint.Cap cap) - 設置線頭的形狀,默認爲 BUTT
setStrokeJoin(Paint.Join join) - 設置拐角的形狀。默認爲 MITER
setStrokeMiter(float miter)- 設置 MITER 型拐角的延長線的最大值
設置圖像的抖動。
抖動是指把圖像從較高色彩深度(便可用的顏色數)向較低色彩深度的區域繪製時,在圖像中有意地插入噪點,經過有規律地擾亂圖像來讓圖像對於肉眼更加真實的作法。
固然這個效果旨在低位色的時候比較有用,例如,ARGB_4444 或者 RGB_565,不過如今Android默認的色彩深度都是32位的ARGB_8888,這個方法的效果沒有那麼明顯。
設置是否使用雙線性過濾來繪製 Bitmap 。
圖像在放大繪製的時候,默認使用的是最近鄰插值過濾,這種算法簡單,但會出現馬賽克現象;而若是開啓了雙線性過濾,就可讓結果圖像顯得更加平滑。
設置圖形的輪廓效果。Android有六種PathEffect:
CornerPathEffect(float radius)
DiscretePathEffect(float segmentLength, float deviation)
DashPathEffect(float[] intervals, float phase)
PathDashPathEffect(Path shape, float advance, float phase, PathDashPathEffect.Style style)
SumPathEffect(PathEffect first, PathEffect second)
ComposePathEffect(PathEffect outerpe, PathEffect innerpe)
舉例
//圖形輪廓效果
//繪製圓角
PathEffect cornerPathEffect = new CornerPathEffect(20);
paint1.setStyle(Paint.Style.STROKE);
paint1.setStrokeWidth(5);
paint1.setPathEffect(cornerPathEffect);
//繪製尖角
PathEffect discretePathEffect = new DiscretePathEffect(20, 5);
paint2.setStyle(Paint.Style.STROKE);
paint2.setStrokeWidth(5);
paint2.setPathEffect(discretePathEffect);
//繪製虛線
PathEffect dashPathEffect = new DashPathEffect(new float[]{20,10, 5, 10}, 0);
paint3.setStyle(Paint.Style.STROKE);
paint3.setStrokeWidth(5);
paint3.setPathEffect(dashPathEffect);
//使用path來繪製虛線
Path path = new Path();//畫一個三角來填充虛線
path.lineTo(40, 40);
path.lineTo(0, 40);
path.close();
PathEffect pathDashPathEffect = new PathDashPathEffect(path, 40, 0, PathDashPathEffect.Style.TRANSLATE);
paint4.setStyle(Paint.Style.STROKE);
paint4.setStrokeWidth(5);
paint4.setPathEffect(pathDashPathEffect);複製代碼
設置陰影圖層,處於目標下層圖層。
舉例
paint1.setTextSize(200);
paint1.setShadowLayer(10, 0, 0, Color.RED);
canvas.drawText("Android", 80, 300 ,paint1);複製代碼
注:在硬件加速開啓的狀況下, setShadowLayer() 只支持文字的繪製,文字以外的繪製必須關閉硬件加速才能正常繪製陰影。若是 shadowColor 是半透明的,陰影的透明度就使用 shadowColor 本身
的透明度;而若是 shadowColor 是不透明的,陰影的透明度就使用 paint 的透明度。
設置圖層遮罩層,處於目標上層圖層。
MaskFilter有兩個子類:
舉例
模糊效果
分別爲:
//設置遮罩圖層,處於目標上層圖層
//關閉硬件加速
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
MaskFilter blurMaskFilter = new BlurMaskFilter(200, BlurMaskFilter.Blur.NORMAL);
paint2.setMaskFilter(blurMaskFilter);
canvas.drawBitmap(bitmapTimo, null, rect1, paint1);
canvas.drawBitmap(bitmapTimo, null, rect2, paint2);複製代碼
注:在硬件加速開啓的狀況下, setMaskFilter(MaskFilter maskfilter)只支持文字的繪製,文字以外的繪製必須關閉硬件加速才能正常繪製陰影。關閉硬件加速能夠調用
View.setLayerType(View.LAYER_TYPE_SOFTWARE, null)或者在Activity標籤裏設置android:hardwareAccelerated="false"。
Canvas實現了Android 2D圖形的繪製,底層基於Skia實現。
Canvas提供了豐富的對象繪製方法,通常都以drawXXX()打頭,繪製的對象包括:
這裏的方法大都很簡單,咱們來描述下期中比較複雜的方法。
弧線
public void drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, @NonNull Paint paint) {
native_drawArc(mNativeCanvasWrapper, left, top, right, bottom, startAngle, sweepAngle,
useCenter, paint.getNativeInstance());
}複製代碼
例如
paint.setStyle(Paint.Style.FILL);//填充模式
canvas.drawArc(200, 100, 800, 500, -110, 100, true, paint);
canvas.drawArc(200, 100, 800, 500, 20, 140, false, paint);
paint.setStyle(Paint.Style.STROKE);//畫線模式
paint.setStrokeWidth(5);
canvas.drawArc(200, 100, 800, 500, 180, 60, false, paint);複製代碼
位圖
@NonNull float[] verts, int vertOffset, @Nullable int[] colors, int colorOffset,
@Nullable Paint paint) - 繪製拉伸位圖**複製代碼
第一個方法很簡單,就是在指定的座標處開始繪製位圖。咱們着重來看看第二個方法,這個方法不是很經常使用(多是計算比較複雜的鍋😓),但這並不影響它強大的功能。
drawBitmapMesh()方法將位圖分爲若干網格,而後對每一個網格進行扭曲處理。咱們先來看看這個方法的參數:
咱們來用drawBitmapMesh()方法實現一個水面漣漪效果。
舉例
/** * 利用Canvas.drawBitmapMeshC()方法對圖像作扭曲處理,模擬水波效果。 * <p> * For more information, you can visit https://github.com/guoxiaoxing or contact me by * guoxiaoxingse@163.com * * @author guoxiaoxing * @since 2017/9/12 下午3:44 */
public class RippleLayout extends FrameLayout {
/** * 圖片橫向、縱向的格樹 */
private final int MESH_WIDTH = 20;
private final int MESH_HEIGHT = 20;
/** * 圖片頂點數 */
private final int VERTS_COUNT = (MESH_WIDTH + 1) * (MESH_HEIGHT + 1);
/** * 原座標數組 */
private final float[] originVerts = new float[VERTS_COUNT * 2];
/** * 轉換後的座標數組 */
private final float[] targetVerts = new float[VERTS_COUNT * 2];
/** * 當前空間的圖像 */
private Bitmap bitmap;
/** * 水波寬度的一半 */
private float rippleWidth = 100f;
/** * 水波擴展的速度 */
private float rippleRadius = 15f;
/** * 水波半徑 */
private float rippleSpeed = 15f;
/** * 水波動畫是否在進行中 */
private boolean isRippling;
public RippleLayout(@NonNull Context context) {
super(context);
}
public RippleLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public RippleLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void dispatchDraw(Canvas canvas) {
if (isRippling && bitmap != null) {
canvas.drawBitmapMesh(bitmap, MESH_WIDTH, MESH_HEIGHT, targetVerts, 0, null, 0, null);
} else {
super.dispatchDraw(canvas);
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
showRipple(ev.getX(), ev.getY());
break;
}
return super.dispatchTouchEvent(ev);
}
/** * 顯示水波動畫 * * @param originX 原點 x 座標 * @param originY 原點 y 座標 */
public void showRipple(final float originX, final float originY) {
if (isRippling) {
return;
}
initData();
if (bitmap == null) {
return;
}
isRippling = true;
//循環次數,經過控件對角線距離計算,確保水波紋徹底消失
int viewLength = (int) getLength(bitmap.getWidth(), bitmap.getHeight());
final int count = (int) ((viewLength + rippleWidth) / rippleSpeed);
Observable.interval(0, 10, TimeUnit.MILLISECONDS)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.take(count + 1)
.subscribe(new Consumer<Long>() {
@Override
public void accept(@NonNull Long aLong) throws Exception {
rippleRadius = aLong * rippleSpeed;
warp(originX, originY);
if (aLong == count) {
isRippling = false;
}
}
});
}
/** * 初始化 Bitmap 及對應數組 */
private void initData() {
bitmap = getCacheBitmapFromView(this);
if (bitmap == null) {
return;
}
float bitmapWidth = bitmap.getWidth();
float bitmapHeight = bitmap.getHeight();
int index = 0;
for (int height = 0; height <= MESH_HEIGHT; height++) {
float y = bitmapHeight * height / MESH_HEIGHT;
for (int width = 0; width <= MESH_WIDTH; width++) {
float x = bitmapWidth * width / MESH_WIDTH;
originVerts[index * 2] = targetVerts[index * 2] = x;
originVerts[index * 2 + 1] = targetVerts[index * 2 + 1] = y;
index += 1;
}
}
}
/** * 圖片轉換 * * @param originX 原點 x 座標 * @param originY 原點 y 座標 */
private void warp(float originX, float originY) {
for (int i = 0; i < VERTS_COUNT * 2; i += 2) {
float staticX = originVerts[i];
float staticY = originVerts[i + 1];
float length = getLength(staticX - originX, staticY - originY);
if (length > rippleRadius - rippleWidth && length < rippleRadius + rippleWidth) {
PointF point = getRipplePoint(originX, originY, staticX, staticY);
targetVerts[i] = point.x;
targetVerts[i + 1] = point.y;
} else {
//復原
targetVerts[i] = originVerts[i];
targetVerts[i + 1] = originVerts[i + 1];
}
}
invalidate();
}
/** * 獲取水波的偏移座標 * * @param originX 原點 x 座標 * @param originY 原點 y 座標 * @param staticX 待偏移頂點的原 x 座標 * @param staticY 待偏移頂點的原 y 座標 * @return 偏移後坐標 */
private PointF getRipplePoint(float originX, float originY, float staticX, float staticY) {
float length = getLength(staticX - originX, staticY - originY);
//偏移點與原點間的角度
float angle = (float) Math.atan(Math.abs((staticY - originY) / (staticX - originX)));
//計算偏移距離
float rate = (length - rippleRadius) / rippleWidth;
float offset = (float) Math.cos(rate) * 10f;
float offsetX = offset * (float) Math.cos(angle);
float offsetY = offset * (float) Math.sin(angle);
//計算偏移後的座標
float targetX;
float targetY;
if (length < rippleRadius + rippleWidth && length > rippleRadius) {
//波峯外的偏移座標
if (staticX > originX) {
targetX = staticX + offsetX;
} else {
targetX = staticX - offsetX;
}
if (staticY > originY) {
targetY = staticY + offsetY;
} else {
targetY = staticY - offsetY;
}
} else {
//波峯內的偏移座標
if (staticX > originY) {
targetX = staticX - offsetX;
} else {
targetX = staticX + offsetX;
}
if (staticY > originY) {
targetY = staticY - offsetY;
} else {
targetY = staticY + offsetY;
}
}
return new PointF(targetX, targetY);
}
/** * 根據寬高,獲取對角線距離 * * @param width 寬 * @param height 高 * @return 距離 */
private float getLength(float width, float height) {
return (float) Math.sqrt(width * width + height * height);
}
/** * 獲取 View 的緩存視圖 * * @param view 對應的View * @return 對應View的緩存視圖 */
private Bitmap getCacheBitmapFromView(View view) {
view.setDrawingCacheEnabled(true);
view.buildDrawingCache(true);
final Bitmap drawingCache = view.getDrawingCache();
Bitmap bitmap;
if (drawingCache != null) {
bitmap = Bitmap.createBitmap(drawingCache);
view.setDrawingCacheEnabled(false);
} else {
bitmap = null;
}
return bitmap;
}
}複製代碼
路徑
public void drawPath(@NonNull Path path, @NonNull Paint paint) {
if (path.isSimplePath && path.rects != null) {
native_drawRegion(mNativeCanvasWrapper, path.rects.mNativeRegion, paint.getNativeInstance());
} else {
native_drawPath(mNativeCanvasWrapper, path.readOnlyNI(), paint.getNativeInstance());
}
}複製代碼
drawPath()能夠繪製自定義圖形,圖形的路徑用Path對象來描述。
Path對象能夠描述不少圖形,具體說來:
Canvas裏的範圍裁切主要有兩類方法:
舉例
clipReact
clipPath
//範圍裁切
canvas.save();//保存畫布
canvas.clipRect(200, 200, 900, 900);
canvas.drawBitmap(bitmapTimo, 100, 100, paint1);
canvas.restore();//恢復畫布
canvas.save();//保存畫布
path.addCircle(500, 500, 300, Path.Direction.CW);
canvas.clipPath(path);
canvas.drawBitmap(bitmapTimo, 100, 100, paint1);
canvas.restore();//恢復畫布複製代碼
關於幾何變換有三種實現方式:
Canvas還提供了對象的位置變換的方法,其中包括:
舉例
canvas.save();//保存畫布
canvas.skew(0, 0.5f);
canvas.drawBitmap(bitmapTimo, null, rect1, paint1);
canvas.restore();//恢復畫布
canvas.save();//保存畫布
canvas.rotate(45, 750, 750);
canvas.drawBitmap(bitmapTimo, null, rect2, paint1);
canvas.restore();//恢復畫布複製代碼
注:1 爲了避免影響其餘繪製操做,在進行變換以前須要調用canvas.save()保存畫布,變換完成之後再調用canvas.restore()來恢復畫布。
2 Canvas幾何變換的順序是相反的,例如咱們在代碼寫了:canvas.skew(0, 0.5f); canvas.rotate(45, 750, 750); 它的實際調用順序是canvas.rotate(45, 750, 750); -> canvas.skew(0, 0.5f)
Matrix也實現了Canvas裏的四種常規變換,它的實現流程以下:
Canvas.concat(matrix):用 Canvas 當前的變換矩陣和 Matrix 相乘,即基於 Canvas 當前的變換,疊加上 Matrix 中的變換。
舉例
//Matrix幾何變換
canvas.save();//保存畫布
matrix.preSkew(0, 0.5f);
canvas.concat(matrix);
canvas.drawBitmap(bitmapTimo, null, rect1, paint1);
canvas.restore();//恢復畫布
canvas.save();//保存畫布
matrix.reset();
matrix.preRotate(45, 750, 750);
canvas.concat(matrix);
canvas.drawBitmap(bitmapTimo, null, rect2, paint1);
canvas.restore();//恢復畫布複製代碼
Matrix除了四種基本的幾何變換,還能夠自定義幾何變換。
這兩個方法都是經過多點的映射的方式來直接設置變換,把指定的點移動到給出的位置,從而發生形變。
舉例
//Matrix幾何變換
canvas.save();//保存畫布
matrix.setPolyToPoly(src, 0, dst, 0, 2);
canvas.concat(matrix);
canvas.drawBitmap(bitmapTimo, 0, 0, paint1);
canvas.restore();//恢復畫布複製代碼
在講解Camera的三維變換以前,咱們須要先理解Camera的座標系系統。
咱們前面說過,Canvas使用的是二維座標系。
而Camera使用的是三維座標系,這裏偷個懶😊,借用凱哥的圖來描述一下。
關於Camera座標系:
好比咱們在Camera座標系裏作個X軸方向的旋轉
Camera的三維變換包括:旋轉、平移與移動相機。
旋轉
平移
移動相機
舉例
旋轉
//Camera三維變換
canvas.save();//保存畫布
camera.save();//保存camera
camera.rotateX(45);
canvas.translate(500, 750);//camera也是默認在原點(0, 0)位置,因此咱們要把畫布平移到圖片中心(500, 750)
camera.applyToCanvas(canvas);
canvas.translate(-500, -750);//翻轉完圖片,再將畫布從圖片中心(500, 750)平移到原點(0, 0)
camera.restore();//恢復camera
canvas.drawBitmap(bitmapTimo, null, rect, paint1);
canvas.restore();//恢復畫布複製代碼
平移
//Camera三維變換
canvas.save();//保存畫布
camera.save();//保存camera
camera.translate(500, 500, 500);
canvas.translate(500, 750);//camera也是默認在原點(0, 0)位置,因此咱們要把畫布平移到圖片中心(500, 750)
camera.applyToCanvas(canvas);
canvas.translate(-500, -750);//翻轉完圖片,再將畫布從圖片中心(500, 750)平移到原點(0, 0)
camera.restore();//恢復camera
canvas.drawBitmap(bitmapTimo, null, rect, paint1);
canvas.restore();//恢復畫布複製代碼
移動相機
//Camera三維變換
canvas.save();//保存畫布
camera.save();//保存camera
camera.setLocation(0, 0, - 1000);//相機往前移動,圖像變小
canvas.translate(500, 750);//camera也是默認在原點(0, 0)位置,因此咱們要把畫布平移到圖片中心(500, 750)
camera.applyToCanvas(canvas);
canvas.translate(-500, -750);//翻轉完圖片,再將畫布從圖片中心(500, 750)平移到原點(0, 0)
camera.restore();//恢復camera
canvas.drawBitmap(bitmapTimo, null, rect, paint1);
canvas.restore();//恢復畫布複製代碼
Path描述了繪製路徑,用它能夠完成不少複雜的圖形繪製。
咱們再來看看Path裏的方法。
例如:addCircle(float x, float y, float radius, Direction dir)
public void addCircle(float x, float y, float radius, Direction dir) {
isSimplePath = false;
native_addCircle(mNativePath, x, y, radius, dir.nativeInt);
}複製代碼
該方法的參數含義:
其餘的方法都是這個方法相似。
直線
//從當前位置,向目標位置畫一條直線,該方法使用相對於原點的絕對座標
public void lineTo(float x, float y) {
isSimplePath = false;
native_lineTo(mNativePath, x, y);
}
//從當前位置,向目標位置畫一條直線,該方法使用相對於當前位置的相對座標
public void rLineTo(float dx, float dy) {
isSimplePath = false;
native_rLineTo(mNativePath, dx, dy);
}複製代碼
當前位置:當前位置指的是最後一次盜用Path的方法的終點位置,初始原點爲(0, 0)
這裏說到當前位置,咱們再提一個方法Path.moveTo(float x, float y),它能夠移動當前位置到一個新的位置。
舉例
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(5);
path.lineTo(300, 400);// 由當前位置 (0, 0) 向 (300, 400) 畫一條直線
path.rLineTo(400, 0);// 由當前位置 (300, 400) 向正右方400像素的位置畫一條直線
canvas.drawPath(path, paint);複製代碼
貝塞爾曲線
貝塞爾曲線:貝塞爾曲線是幾何上的一種曲線。它經過起點、控制點和終點來描述一條曲線,主要用於計算機圖形學。簡單來講,貝塞爾曲線就是將任意一條曲線轉換爲精確的數學公式。
在貝塞爾曲線中,有兩類點:
一階貝塞爾曲線
B(t)爲時間爲t時的座標,P0爲起點,P1爲終點。
二階貝塞爾曲線
三階貝塞爾曲線
貝塞爾曲線的模擬可使用bezier-curve
咱們再來看看Path類提供的關於貝塞爾曲線的方法。
//二階貝塞爾曲線,絕對座標,(x1, y1)表示控制點,(x2, y2)表示終點
public void quadTo(float x1, float y1, float x2, float y2) {
isSimplePath = false;
native_quadTo(mNativePath, x1, y1, x2, y2);
}
//二階貝塞爾曲線,相對座標
public void rQuadTo(float dx1, float dy1, float dx2, float dy2) {
isSimplePath = false;
native_rQuadTo(mNativePath, dx1, dy1, dx2, dy2);
}
//三階貝塞爾曲線,絕對座標,(x1, y1)、(x2, y2)表示控制點,(x3, y3)表示終點
public void cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) {
isSimplePath = false;
native_cubicTo(mNativePath, x1, y1, x2, y2, x3, y3);
}
//三階貝塞爾曲線,相對座標
public void rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3) {
isSimplePath = false;
native_rCubicTo(mNativePath, x1, y1, x2, y2, x3, y3);
}複製代碼
咱們來用貝塞爾曲線實現一個杯中倒水效果。
舉例
/** * 控制點的X座標不斷左右移動,造成波浪效果。 * <p> * For more information, you can visit https://github.com/guoxiaoxing or contact me by * guoxiaoxingse@163.com * * @author guoxiaoxing * @since 2017/9/11 下午6:11 */
public class WaveView extends View {
private static final String TAG = "WaveView";
/** * 波浪從屏幕外開始,在屏幕外結束,這樣效果更真實 */
private static final float EXTRA_DISTANCE = 200;
private Path mPath;
private Paint mPaint;
/** * 控件寬高 */
private int mWidth;
private int mHeight;
/** * 控制點座標 */
private float mControlX;
private float mControlY;
/** * 波浪峯值 */
private float mWaveY;
/** * 是否移動控制點 */
private boolean mMoveControl = true;
public WaveView(Context context) {
super(context);
init();
}
public WaveView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public WaveView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = w;
mHeight = h;
mControlY = mHeight - mHeight / 8;
mWaveY = mHeight - mHeight / 32;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//波浪從屏幕外開始,效果更真實
mPath.moveTo(-EXTRA_DISTANCE, mWaveY);
//二階貝塞爾曲線
mPath.quadTo(mControlX, mControlY, mWidth + EXTRA_DISTANCE, mWaveY);
//閉合曲線
mPath.lineTo(mWidth, mHeight);
mPath.lineTo(0, mHeight);
mPath.close();
canvas.drawPath(mPath, mPaint);
//mControlX座標在 -EXTRA_DISTANCE ~ mWidth + EXTRA_DISTANCE 範圍內,先自增再自減,左右移動
//造成波浪效果
if (mControlX <= -EXTRA_DISTANCE) {
mMoveControl = true;
} else if (mControlX >= mWidth + EXTRA_DISTANCE) {
mMoveControl = false;
}
mControlX = mMoveControl ? mControlX + 20 : mControlX - 20;
//水面不斷上升
if (mControlY >= 0) {
mControlY -= 2;
mWaveY -= 2;
}
Log.d(TAG, "mControlX: " + mControlX + " mControlY: " + mControlY + " mWaveY: " + mWaveY);
mPath.reset();
invalidate();
}
private void init() {
mPath = new Path();
mPaint = new Paint();
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mPaint.setColor(Color.parseColor("#4CAF50"));
}
}複製代碼
弧線
//畫弧線
public void arcTo(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo) {
isSimplePath = false;
native_arcTo(mNativePath, left, top, right, bottom, startAngle, sweepAngle, forceMoveTo);
}複製代碼
咱們來看看這個方法的參數:
注:能夠發現,這個方法與一樣用來畫弧線的方法Canvas.drawArc()少了個boolean useCenter參數,這是由於arcTo()方法只用來畫弧線。
public void setFillType(FillType ft) - 設置填充方式
方法用來設置填充方式,填充的方式有四種:
WINDING:non-zero winding rule,非零環繞數原則,該原則基於全部圖形的繪製都有繪製方向(前面提到的Direction描述的順時針與逆向時針),對於平面上的任意一點,向任意方向射出一條射線,射線遇到每一個順時針
的交點則加1,遇到逆時針的交點則減1,最後的結果若是不爲0,則認爲該點在圖形內部,染色。若是結果爲0,則認爲該點在圖形外部,不染色。
EVEN_ODD:even-odd rule,奇偶原則,對於平面上的任意一點,向任意方向射出一條射線,這條射線與圖形相交(不是相切)的次數爲奇數則說明這個點在圖形內部,則進行染色。若爲偶數則認爲在圖形外部,不進行染色。
這是一中交叉染色的狀況。