自定義View-基礎(chǔ)

自定義繪制

  • 自定義繪制的方式是重寫繪制方法,其中最常用的是 onDraw()
  • 繪制的關(guān)鍵是 Canvas 的使用
    Canvas 的繪制類方法: drawXXX() (關(guān)鍵參數(shù):Paint)
    Canvas 的輔助類方法:范圍裁切和幾何變換
  • 可以使用不同的繪制方法來控制遮蓋關(guān)系
Canvas.drawXXX() 和 Paint 基礎(chǔ)
  • Canvas 類下的所有 draw- 打頭的方法,例如 drawCircle() drawBitmap()。
  • Paint 類的幾個最常用的方法。
    paint提供基本信息之外的所有風(fēng)格信息,例如顏色、線條粗細、陰影等。
  1. Paint.setColor(int color) 設(shè)置顏色
  2. Paint.setStyle(Paint.Style style)設(shè)置繪制模式
    FILL 是填充模式,STROKE 是畫線模式(即勾邊模式),F(xiàn)ILL_AND_STROKE 是兩種模式一并使用:既畫線又填充。它的默認值是 FILL,填充模式。
  3. Paint.setStrokeWidth(float width) 設(shè)置線條寬度
    在 STROKE 和 FILL_AND_STROKE 下來設(shè)置線條的寬度
  4. Paint.setAntiAlias(boolean aa) 設(shè)置抗鋸齒開關(guān)
    要在 new Paint() 的時候加上一個 ANTI_ALIAS_FLAG 參數(shù)就行
  5. Paint.setTextSize(float textSize) 設(shè)置文字大小

Canvas.drawColor(@ColorInt int color) 顏色填充
drawCircle(float centerX, float centerY, float radius, Paint paint) 畫圓
drawRect(float left, float top, float right, float bottom, Paint paint) 畫矩形
drawPoint(float x, float y, Paint paint) 畫點
drawPoints(float[] pts, int offset, int count, Paint paint) / drawPoints(float[] pts, Paint paint) 畫點(批量)
drawOval(float left, float top, float right, float bottom, Paint paint) 畫橢圓
drawLine(float startX, float startY, float stopX, float stopY, Paint paint) 畫線
drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, Paint paint) 畫圓角矩形
drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint) 繪制弧形或扇形
drawPath(Path path, Paint paint) 畫自定義圖形
drawBitmap(Bitmap bitmap, float left, float top, Paint paint) 畫 Bitmap
drawText(String text, float x, float y, Paint paint) 繪制文字

Path 方法
  • Path 方法第一類:直接描述路徑。
    第一組: addXxx() ——添加子圖形
    如:addCircle(float x, float y, float radius, Direction dir) 添加圓
    第二組:xxxTo() ——畫線(直線或曲線)
  1. lineTo(float x, float y) / rLineTo(float x, float y) 畫直線
  2. quadTo(float x1, float y1, float x2, float y2) / rQuadTo(float dx1, float dy1, float dx2, float dy2) 畫二次貝塞爾曲線
  3. cubicTo(float x1, float y1, float x2, float y2, float x3, float y3) / rCubicTo(float x1, float y1, float x2, float y2, float x3, float y3) 畫三次貝塞爾曲線
  4. moveTo(float x, float y) / rMoveTo(float x, float y) 移動到目標位置
  5. arcTo(RectF oval, float startAngle, float sweepAngle, boolean forceMoveTo) / arcTo(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo) / arcTo(RectF oval, float startAngle, float sweepAngle) 畫弧形
  6. addArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle) / addArc(RectF oval, float startAngle, float sweepAngle)
    addArc() 只是一個直接使用了 forceMoveTo = true 的簡化版 arcTo()
  7. close() 封閉當前子圖形
  • Path 方法第二類:輔助的設(shè)置或計算
    Path.setFillType(Path.FillType ft) 設(shè)置填充方式

Paint

1.顏色
  • 基本顏色
    Canvas 的顏色填充類方法 drawColor/RGB/ARGB() 的顏色,是直接寫在方法的參數(shù)里,通過參數(shù)來設(shè)置的; drawBitmap() 的顏色,是直接由 Bitmap 對象來提供的;
    除此之外,是圖形和文字的繪制,它們的顏色就需要使用 paint 參數(shù)來額外設(shè)置了。

Paint 設(shè)置顏色的方法有兩種:

  1. 直接設(shè)置顏色
    paint.setColor(Color.parseColor("#009688"));
    setARGB(int a, int r, int g, int b)
    setShader(Shader shader) 設(shè)置 Shader
    1.1 LinearGradient 線性漸變
    LinearGradient(float x0, float y0, float x1, float y1, int color0, int color1, Shader.TileMode tile)
    TileMode 一共有 3 個值可選:CLAMP 會在端點之外延續(xù)端點處的顏色;MIRROR 是鏡像模式;REPEAT 是重復(fù)模式。
    1.2 RadialGradient 輻射漸變
    RadialGradient(float centerX, float centerY, float radius, int centerColor, int edgeColor, TileMode tileMode)
    1.3 SweepGradient 掃描漸變
    SweepGradient(float cx, float cy, int color0, int color1)
    1.4 BitmapShader
    BitmapShader(Bitmap bitmap, Shader.TileMode tileX, Shader.TileMode tileY)
    1.5 ComposeShader 混合著色器
    所謂混合,就是把兩個 Shader 一起使用。
    ComposeShader(Shader shaderA, Shader shaderB, PorterDuff.Mode mode)
    mode: 兩個 Shader 的疊加模式
    PorterDuff.Mode :
    PorterDuff.Mode.SRC_OVER 把源圖像直接鋪在目標圖像上
    PorterDuff.Mode.DST_OUT 挖空效果
    PorterDuff.Mode.DST_IN 蒙版摳圖

  2. setColorFilter(ColorFilter colorFilter) 基于原始顏色的過濾
    ColorFilter 并不直接使用,而是使用它的子類。它共有三個子類:
    2.1 LightingColorFilter(int mul, int add)
    模擬簡單的光照效果的
    2.2 PorterDuffColorFilter(int color, PorterDuff.Mode mode)
    使用一個指定的顏色和一種指定的 PorterDuff.Mode 來與繪制對象進行合成
    2.3 ColorMatrixColorFilter

  3. setXfermode(Xfermode xfermode)
    設(shè)置繪制內(nèi)容和 View 中已有內(nèi)容的混合計算方式
    直接用 Xfermode 的一個子類PorterDuffXfermode
    要想使用 setXfermode() 正常繪制,必須使用Canvas.saveLayer()離屏緩存 (Off-screen Buffer) 把內(nèi)容繪制在額外的層上,再把繪制好的內(nèi)容貼回 View 中


    image.png
2.效果

抗鋸齒、填充/輪廓、線條寬度等等

  1. setAntiAlias (boolean aa) 設(shè)置抗鋸齒
    或者Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
  2. setStyle(Paint.Style style)
    用來設(shè)置圖形是線條風(fēng)格還是填充風(fēng)格的(也可以二者并用)
    Paint.Style.FILL填充
    Paint.Style.STROKE畫線
    Paint.Style.FILL_AND_STROKE填充 + 畫線
  3. 線條形狀
    3.1 setStrokeWidth(float width)
    設(shè)置線條寬度。單位為像素,默認值是 0。
    3.2 setStrokeCap(Paint.Cap cap)
    設(shè)置線頭的形狀。線頭形狀有三種:BUTT 平頭、ROUND 圓頭、SQUARE 方頭。默認為 BUTT
    3.3 setStrokeJoin(Paint.Join join)
    設(shè)置拐角的形狀。有三個值可以選擇:MITER 尖角、 BEVEL 平角和 ROUND 圓角。默認為 MITER。
    3.4 setStrokeMiter(float miter)
    線條在 Join 類型為 MITER 時對于 MITER 的長度限制
    miter 參數(shù)是對于轉(zhuǎn)角長度的限制
  4. 色彩優(yōu)化
    4.1 setDither(boolean dither)
    設(shè)置抖動來優(yōu)化色彩深度降低時的繪制效果
    抖動更多的作用是在圖像降低色彩深度繪制時,避免出現(xiàn)大片的色帶與色塊
    4.2 setFilterBitmap(boolean filter)
    設(shè)置是否使用雙線性過濾來繪制 Bitmap
    設(shè)置雙線性過濾來優(yōu)化 Bitmap 放大繪制的效果
  5. setPathEffect(PathEffect effect)
    使用 PathEffect 來給圖形的輪廓設(shè)置效果。對 Canvas 所有的圖形繪制有效
    5.1.CornerPathEffect(float radius)
    把所有拐角變成圓角。.
    5.2.DiscretePathEffect(float segmentLength, float deviation
    把線條進行隨機的偏離,讓輪廓變得亂七八糟
    5.3.DashPathEffect(float[] intervals, float phase)
    使用虛線來繪制線條。
    第一個參數(shù) intervals 是一個數(shù)組,它指定了虛線的格式:數(shù)組中元素必須為偶數(shù)(最少是 2 個),按照「畫線長度、空白長度、畫線
    長度、空白長度」……的順序排列;第二個參數(shù) phase 是虛線的偏移量。
    5.4.PathDashPathEffect(Path shape, float advance, float phase, PathDashPathEffect.Style style)
    它是使用一個 Path 來繪制「虛線」
    shape 參數(shù)是用來繪制的 Path ; advance 是兩個相鄰的 shape 段之間的間隔,不過注意,這個間隔是兩個 shape 段的起點的間隔,而不是前一個的終點和后一個的起點的距離; phase 和 DashPathEffect 中一樣,是虛線的偏移;最后一個參數(shù) style,是用來指定拐彎改變的時候 shape 的轉(zhuǎn)換方式。
    style 的類型為 PathDashPathEffect.Style ,是一個 enum ,具體有三個值:TRANSLATE:位移,ROTATE:旋轉(zhuǎn),MORPH:變體
    5.5.SumPathEffect
    分別按照兩種 PathEffect 分別對目標進行繪制
    5.6.ComposePathEffect(PathEffect outerpe, PathEffect innerpe)
    它是先對目標 Path 使用一個 PathEffect,然后再對這個改變后的 Path 使用另一個 PathEffect
  6. setShadowLayer(float radius, float dx, float dy, int shadowColor)
    在之后的繪制內(nèi)容下面加一層陰影
    radius 是陰影的模糊范圍; dx dy 是陰影的偏移量; shadowColor 是陰影的顏色
  7. setMaskFilter(MaskFilter maskfilter)
    設(shè)置的是在繪制層上方的附加效果,是基于整個畫面來進行過濾
    7.1 BlurMaskFilter(float radius, BlurMaskFilter.Blur style)
    模糊效果的 MaskFilter
    radius 參數(shù)是模糊的范圍, style 是模糊的類型:
    NORMAL: 內(nèi)外都模糊繪制;SOLID: 內(nèi)部正常繪制,外部模糊;INNER: 內(nèi)部模糊,外部不繪制
    7.2 EmbossMaskFilter(float[] direction, float ambient, float specular, float blurRadius)
    浮雕效果的 MaskFilter
    direction 是一個 3 個元素的數(shù)組,指定了光源的方向; ambient 是環(huán)境光的強度,數(shù)值范圍是 0 到 1; specular 是炫光的系數(shù); blurRadius 是應(yīng)用光線的范圍
  8. 獲取繪制的 Path
    8.1 getFillPath(Path src, Path dst)
    獲取這個實際 Path
    src 是原 Path ,而 dst 就是實際 Path 的保存位置
    8.2 getTextPath(String text, int start, int end, float x, float y, Path path) / getTextPath(char[] text, int index, int count, float x, float y, Path path)
3.drawText() 相關(guān)
4.初始化類

是用來初始化 Paint 對象,或者是批量設(shè)置 Paint 的多個屬性的方法
1.reset()
重置 Paint 的所有屬性為默認值
2.set(Paint src)
把 src 的所有屬性全部復(fù)制過來。相當于調(diào)用 src 所有的 get 方法,然后調(diào)用這個 Paint 對應(yīng)的 set 方法來設(shè)置它們
3.setFlags(int flags)
批量設(shè)置 flags
如:paint.setFlags(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);

文字繪制

1 Canvas 繪制文字的方式

1.1 drawText(String text, float x, float y, Paint paint)
text 是文字內(nèi)容,x 和 y 是文字的坐標。但需要注意:這個坐標并不是文字的左上角,而是一個與左下角比較接近的位置
drawText() 參數(shù)中的 y ,指的是文字的基線( baseline )的位置
1.2 drawTextOnPath(String text, Path path, float hOffset, float vOffset, Paint paint)
沿著一條 Path 來繪制文字
hOffset 和 vOffset。它們是文字相對于 Path 的水平偏移量和豎直偏移量,利用它們可以調(diào)整文字的位置。
1.3 StaticLayout
StaticLayout 多行文字的繪制,支持換行,它既可以為文字設(shè)置寬度上限來讓文字自動換行,也會在 \n 處主動換行
StaticLayout(CharSequence source, TextPaint paint, int width, Layout.Alignment align, float spacingmult, float spacingadd, boolean includepad)
width 是文字區(qū)域的寬度,文字到達這個寬度后就會自動換行;align 是文字的對齊方向;spacingmult 是行間距的倍數(shù),通常情況下填 1 就好;spacingadd 是行間距的額外增加值,通常情況下填 0 就好;includeadd 是指是否在文字上下添加額外的空間,來避免某些過高的字符的繪制出現(xiàn)越界。

2 Paint 對文字繪制的輔助
  • 設(shè)置顯示效果類
    2.1 setTextSize(float textSize)
    設(shè)置文字大小
    2.2 setTypeface(Typeface typeface)
    設(shè)置字體
    2.3 setFakeBoldText(boolean fakeBoldText)
    是否使用偽粗體
    2.4 setStrikeThruText(boolean strikeThruText)
    是否加刪除線
    2.5 setUnderlineText(boolean underlineText)
    是否加下劃線
    2.6 setTextSkewX(float skewX)
    設(shè)置文字橫向錯切角度。其實就是文字傾斜度
    2.7 setTextScaleX(float scaleX)
    設(shè)置文字橫向放縮
    2.8 setLetterSpacing(float letterSpacing)
    設(shè)置字符間距。默認值是 0、
    2.9 setFontFeatureSettings(String settings)
    用 CSS 的 font-feature-settings 的方式來設(shè)置文字
    2.10 setTextAlign(Paint.Align align)
    設(shè)置文字的對齊方式。一共有三個值:LEFT CENTER 和 RIGHT,默認值為 LEFT
    2.11 setTextLocale(Locale locale)
    設(shè)置繪制所使用的 Locale
  • 測量文字尺寸類
    1 getFontSpacing()
    獲取推薦的行距,即推薦的兩行文字的 baseline 的距離
    2 FontMetircs getFontMetrics()
    獲取 Paint 的 FontMetrics
    FontMetrics 是個相對專業(yè)的工具類,它提供了幾個文字排印方面的數(shù)值:ascent, descent, top, bottom, leading。
    FontMetrics.ascent:float 類型 baseline - ascent 負值
    FontMetrics.descent:float 類型, descent - baseline 正值
    FontMetrics.top:float 類型, baseline - top 負值
    FontMetrics.bottom:float 類型,bottom - baseline 正值
    FontMetrics.leading:float 類型, 上行bottom - 下行top
    3 getTextBounds(String text, int start, int end, Rect bounds)
    獲取文字的顯示范圍。
    text 是要測量的文字,start 和 end 分別是文字的起始和結(jié)束位置,bounds 是存儲文字顯示范圍的對象,方法在測算完成之后會把結(jié)果寫進 bounds。
paint.setStyle(Paint.Style.FILL);
canvas.drawText(text, offsetX, offsetY, paint);

paint.getTextBounds(text, 0, text.length(), bounds);
bounds.left += offsetX;
bounds.top += offsetY;
bounds.right += offsetX;
bounds.bottom += offsetY;
paint.setStyle(Paint.Style.STROKE);
canvas.drawRect(bounds, paint);

4 float measureText(String text)
測量的是文字繪制時所占用的寬度
5 getTextWidths(String text, float[] widths)
獲取字符串中每個字符的寬度,并把結(jié)果填入?yún)?shù) widths
6 int breakText(String text, boolean measureForwards, float maxWidth, float[] measuredWidth)
測量文字寬度的,breakText() 是在給出寬度上限的前提下測量文字的寬度。如果文字的寬度超出了上限,那么在臨近超限的位置截斷文字。
breakText() 的返回值是截取的文字個數(shù),參數(shù)中, text 是要測量的文字;measureForwards 表示文字的測量方向,true 表示由左往右測量;maxWidth 是給出的寬度上限;measuredWidth 是用于接受數(shù)據(jù)
這個方法可以用于多行文字的折行計算
7 光標相關(guān)
7.1 getRunAdvance(CharSequence text, int start, int end, int contextStart, int contextEnd, boolean isRtl, int offset)
計算出某個字符處光標的 x 坐標
start end 是文字的起始和結(jié)束坐標;contextStart contextEnd 是上下文的起始和結(jié)束坐標;isRtl 是文字的方向;offset 是字數(shù)的偏移,即計算第幾個字符處的光標。
7.2 getOffsetForAdvance(CharSequence text, int start, int end, int contextStart, int contextEnd, boolean isRtl, float advance)
給出一個位置的像素值,計算出文字中最接近這個位置的字符偏移量(即第幾個字符最接近這個坐標)。
text 是要測量的文字;start end 是文字的起始和結(jié)束坐標;contextStart contextEnd 是上下文的起始和結(jié)束坐標;isRtl 是文字方向;advance 是給出的位置的像素值。填入?yún)?shù),對應(yīng)的字符偏移量將作為返回值返回。
8 hasGlyph(String string)
檢查指定的字符串中是否是一個單獨的字形 (glyph)

Canvas 對繪制的輔助

1 范圍裁切

1.1 clipRect(left, top, right, bottom)
記得要加上 Canvas.save() 和 Canvas.restore() 來及時恢復(fù)繪制范圍

canvas.save();
canvas.clipRect(left, top, right, bottom);
canvas.drawBitmap(bitmap, x, y, paint);
canvas.restore();

1.2 clipPath()

2 幾何變換
2.1 使用 Canvas 來做常見的二維變換:

2.1.1 Canvas.translate(float dx, float dy) 平移
dx 和 dy 表示橫向和縱向的位移。
2.1.2 Canvas.rotate(float degrees, float px, float py) 旋轉(zhuǎn)
degrees 是旋轉(zhuǎn)角度,方向是順時針為正向; px 和 py 是軸心的位置
2.1.3 Canvas.scale(float sx, float sy, float px, float py) 縮放
sx sy 是橫向和縱向的放縮倍數(shù); px py 是放縮的軸心
2.1.4 skew(float sx, float sy) 錯切
sx 和 sy 是 x 方向和 y 方向的錯切系數(shù)

2.2 使用 Matrix 來做變換

2.2.1 使用 Matrix 來做常見變換
Matrix 做常見變換的方式:

  1. 創(chuàng)建 Matrix 對象;
  2. 調(diào)用 Matrix 的 pre/postTranslate/Rotate/Scale/Skew() 方法來設(shè)置幾何變換;
  3. 使用 Canvas.setMatrix(matrix) 或 Canvas.concat(matrix) 來把幾何變換應(yīng)用到 Canvas。
Matrix matrix = new Matrix();
...
matrix.reset();
matrix.postTranslate();
matrix.postRotate();

canvas.save();
canvas.concat(matrix);
canvas.drawBitmap(bitmap, x, y, paint);
canvas.restore();

注:把 Matrix 應(yīng)用到 Canvas, 盡量用 concat(matrix)
2.2.2 使用 Matrix 來做自定義變換
Matrix.setPolyToPoly(float[] src, int srcIndex, float[] dst, int dstIndex, int pointCount) 用點對點映射的方式設(shè)置變換
src 和 dst 是源點集和目標點集;srcIndex 和 dstIndex 是第一個點的偏移;pointCount 是采集的點的個數(shù)(個數(shù)不能大于 4,因為大于 4 個點就無法計算變換了)

2.3 使用 Camera 來做三維變換

2.3.1 Camera.rotate() 三維旋轉(zhuǎn)
Camera.rotate
() 一共有四個方法: rotateX(deg) rotateY(deg) rotateZ(deg) rotate(x, y, z)。
Camera 和 Canvas 一樣也需要保存和恢復(fù)狀態(tài)才能正常繪制

canvas.save();

camera.save(); // 保存 Camera 的狀態(tài)
camera.rotateX(30); // 旋轉(zhuǎn) Camera 的三維空間
camera.applyToCanvas(canvas); // 把旋轉(zhuǎn)投影到 Canvas
camera.restore(); // 恢復(fù) Camera 的狀態(tài)

canvas.drawBitmap(bitmap, point1.x, point1.y, paint);
canvas.restore();

如果你需要圖形左右對稱,需要配合上 Canvas.translate(),在三維旋轉(zhuǎn)之前把繪制內(nèi)容的中心點移動到原點,即旋轉(zhuǎn)的軸心,然后在三維旋轉(zhuǎn)后再把投影移動回來:

canvas.save();

camera.save(); // 保存 Camera 的狀態(tài)
camera.rotateX(30); // 旋轉(zhuǎn) Camera 的三維空間
canvas.translate(centerX, centerY); // 旋轉(zhuǎn)之后把投影移動回來
camera.applyToCanvas(canvas); // 把旋轉(zhuǎn)投影到 Canvas
canvas.translate(-centerX, -centerY); // 旋轉(zhuǎn)之前把繪制內(nèi)容移動到軸心(原點)
camera.restore(); // 恢復(fù) Camera 的狀態(tài)

canvas.drawBitmap(bitmap, point1.x, point1.y, paint);
canvas.restore();

注:Canvas 的幾何變換順序是反的,所以要把移動到中心的代碼寫在下面,把從中心移動回來的代碼寫在上面。
2.3.2 Camera.translate(float x, float y, float z) 移動
2.3.3 Camera.setLocation(x, y, z) 設(shè)置虛擬相機的位置
Camera 的位置單位是英寸,英寸和像素的換算單位在是 72 像素
在 Camera 中,相機的默認位置是 (0, 0, -8)(英寸)。8 x 72 = 576,所以它的默認位置是 (0, 0, -576)(像素)。
Camera.setLocation(x, y, z) 的 x 和 y 參數(shù)一般不會改變,直接填 0 就好

繪制順序

1 super.onDraw() 前 or 后?

自定義繪制最基本的形態(tài):繼承 View 類,在 onDraw() 中完全自定義它的繪制
在 View.java 的源碼中,onDraw() 是空實現(xiàn),把繪制代碼全都寫在了 super.onDraw() 的下面,上面或者刪除都一樣
基于已有控件的自定義繪制,即繼承一個具有某種功能的控件,就不能不考慮 super.onDraw() 了
1.1 寫在 super.onDraw() 的下面
繪制內(nèi)容就會蓋住控件原來的內(nèi)容
1.2 寫在 super.onDraw() 的上面
繪制的內(nèi)容會被控件的原內(nèi)容蓋住

2 dispatchDraw():繪制子 View 的方法

由于 View 是沒有子 View 的,所以一般來說 dispatchDraw() 這個方法只對 ViewGroup(以及它的子類)有意義。


image.png

2.1 寫在 super.dispatchDraw() 的下面
只要重寫 dispatchDraw(),并在 super.dispatchDraw() 的下面寫上你的繪制代碼,這段繪制代碼就會發(fā)生在子 View 的繪制之后,從而讓繪制內(nèi)容蓋住子 View 了。
2.2 寫在 super.dispatchDraw() 的上面
繪制內(nèi)容會出現(xiàn)在主體內(nèi)容和子 View 之間

3 繪制過程簡述

一個完整的繪制過程會依次繪制以下幾個內(nèi)容:
1.背景
背景的繪制在drawBackground() 方法中,這個方法是private的,不能重寫,只能通過xml 布局文件的 android:background 屬性以及 Java 代碼的 View.setBackgroundXxx() 方法
2.主體(onDraw())
3.子 View(dispatchDraw())
4.滑動邊緣漸變和滑動條
5.前景
而第 4、5 兩步——滑動邊緣漸變和滑動條以及前景,這兩部分被合在一起放在了 onDrawForeground() 方法里,這個方法是可以重寫的
滑動邊緣漸變和滑動條可以通過 xml 的 android:scrollbarXXX 系列屬性或 Java 代碼的 View.setXXXScrollbarXXX() 系列方法來設(shè)置;前景可以通過 xml 的 android:foreground 屬性或 Java 代碼的 View.setForeground() 方法來設(shè)置


image.png
4 onDrawForeground()

這個方法是 API 23 才引入
在 onDrawForeground() 中,會依次繪制滑動邊緣漸變、滑動條和前景
4.1 寫在 super.onDrawForeground() 的下面
繪制內(nèi)容將會蓋住滑動邊緣漸變、滑動條和前景。
4.2 寫在 super.onDrawForeground() 的上面
繪制內(nèi)容會蓋住子 View,但會被滑動邊緣漸變、滑動條以及前景蓋?。?br> 4.3 想在滑動邊緣漸變、滑動條和前景之間插入繪制代碼?
不行。

5 draw() 總調(diào)度方法

一個 View 的整個繪制過程都發(fā)生在 draw() 方法里

// View.java 的 draw() 方法的簡化版大致結(jié)構(gòu)(是大致結(jié)構(gòu),不是源碼哦):
public void draw(Canvas canvas) {
    ...
    drawBackground(Canvas); // 繪制背景(不能重寫)
    onDraw(Canvas); // 繪制主體
    dispatchDraw(Canvas); // 繪制子 View
    onDrawForeground(Canvas); // 繪制滑動相關(guān)和前景
    ...
}
image.png

5.1 寫在 super.draw() 的下面
繪制內(nèi)容會蓋住其他的所有繪制內(nèi)容
5.2 寫在 super.draw() 的上面
這部分繪制內(nèi)容會被其他所有的內(nèi)容蓋住,包括背景。


image.png

注:在 ViewGroup 的子類中重寫除 dispatchDraw() 以外的繪制方法時,可能需要調(diào)用 setWillNotDraw(false);在重寫的方法有多個選擇時,優(yōu)先選擇 onDraw()

硬件加速

指的是把某些計算工作交給專門的硬件來做,而不是和普通的計算工作一樣交給CPU處理。
在 Android 里,硬件加速專指把 View 中繪制的計算工作交給 GPU 來處理;
在硬件加速關(guān)閉時,繪制內(nèi)容被CPU轉(zhuǎn)換成實際的像素,然后直接渲染到屏幕;
硬件加速的原因:
1.用了GPU,繪制變快了
2.繪制機制的改變,導(dǎo)致界面內(nèi)容改變時的刷新效率極大提高

布局

布局過程:
就是程序在運行時利用布局文件的代碼來計算出實際尺寸的過程
布局過程的工作內(nèi)容:測量階段和布局階段

  1. 測量階段:從上到下遞歸地調(diào)用每個 View 或者 ViewGroup 的 measure() 方法,測量他們的尺寸并計算它們的位置;
  2. 布局階段:從上到下遞歸地調(diào)用每個 View 或者 ViewGroup 的 layout() 方法,把測得的它們的尺寸和位置賦值給它們。
View 或 ViewGroup 的布局過程

測量階段:

  1. View: View在onMeasure()中會計算出自己的尺寸然后保存;
  2. ViewGroup: ViewGroup在onMeasure()中會調(diào)用所有子View的measure()讓它們進行自我測量,并根據(jù)子View計算出的期望尺寸來計算它們的實際尺寸和位置然后保存,同時,它會根據(jù)子View的尺寸和位置來計算出自己的尺寸然后保存;

布局階段:

  1. View: 由于沒有子View,所以View的onLayout()什么也不做
  2. ViewGroup:ViewGroup在onLayout()中會調(diào)用自己的子View的layout()方法,把它們的尺寸和位置傳給它們,讓它們完成自我的內(nèi)部布局;

布局過程自定義的方式

一. 重寫onMeasure()來修改已有View的尺寸;
  1. 重寫onMeasure()方法,并在里面調(diào)用super.onMeasure(), 觸發(fā)原有的自我測量;
  2. 在super.onMeasure()的下面用getMeasureWidth()和getMeasuredHeight()取出之前計算出的結(jié)果,然后把它們修改成新的尺寸,再保存下來;
  3. 調(diào)用setMeasuredDimension()來保存新的結(jié)果;
二. 重寫onMeasure()來全新自定義View的尺寸;
  1. 重寫onMeasure(),并計算出View的尺寸
  2. 使用resolveSize()來讓子View的計算結(jié)果符合父View的限制;

onMeasure的兩個參數(shù)就是父View傳進來的限制,一個是寬度限制,一個是高度限制,在計算完寬度和高度之后,分別調(diào)用一次resolveSize()方法,把你計算出來的寬度和高度以及對應(yīng)的父View的限制一起傳進去,返回的結(jié)果就是符合父View限制的修正之后的尺寸;然后把這個修正之后的尺寸用setMeasuredDimension保存起來就行了。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    measuredWidth = ...;
    measuredHeight = ...;
    measuredWidth = resolveSize(measuredWidth, widthMeasuredSpec);
    measuredHeight = resolveSize(measuredHeight, heightMeasuredSpec);

    setMeasuredDimension(measuredWidth, measuredHeight);
}

public static int resolveSize(int size, int measureSpec) {
    final int specMode = MeasureSpec.getMode(measureSpec);
    final int specSize = MeasureSpec.getSize(measureSpec);
    switch(specMode){
        case MeasureSpec.UNSPECIFIE:
              return size;
        case MeasureSpec.AT_MOST:
              if(size<=specSize){
                  return size;
              }else{
                  return specSize;
              }
        case MeasureSpec.EXACTLY:
              return specSize;
        default:
              return size;
    }
}

父View的尺寸限制:

  1. 由來:開發(fā)者的要求(布局文件中l(wèi)ayout_打頭的屬性)經(jīng)過父View處理計算后的更精確的要求;
  2. 限制的類型:
    UNSPECIFIED不限制,AT_MOST限制上限,EXACTLY限制固定值
三. 重寫onMeasure()和onLayout()來全新定制自定義ViewGroup的內(nèi)部布局;
1. 重寫onMeasure()來計算內(nèi)部布局

也就是子View的位置和尺寸,以及自己的尺寸

onMeasure()的重寫:

  1. 調(diào)用每個子View的measure(), 讓子View自我測量;
    計算子View尺寸的關(guān)鍵: 在于measure()方法的兩個參數(shù),也就是子View的兩個MeasureSpec的計算;
    子View的MeasureSpec的計算方式:
    1.結(jié)合開發(fā)者的要求(xml中l(wèi)ayout_打頭的屬性)和自己的可用空間(自己的尺寸上線 - 已用尺寸)
    2.尺寸上限根據(jù)自己的MeasureSpec中的mode而定,EXACTLY/AT_MOST:尺寸上限為MeasureSpec中的size,UNSPECIFIED:尺寸無上限;
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);

            LayoutParams lp = childView.getLayoutParams();
            int selfWidthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
            int selfWidthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
            switch (lp.width) {
                case MATCH_PARENT:
                    if (selfWidthSpecMode == EXACTLY || selfWidthSpecMode == AT_MOST) {
                        childWidthSpec = MeasureSpec.makeMeasureSpec(selfWidthSpecSize - usedWidth, EXACTLY);//可用寬度
                    } else {//UNSPECIFIED
                        childWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
                    }
                    break;
                case WRAP_CONTENT:
                    if (selfWidthSpecMode == EXACTLY || selfWidthSpecMode == AT_MOST) {
                        childWidthSpec = MeasureSpec.makeMeasureSpec(selfWidthSpecSize - usedWidth, AT_MOST);//可用寬度
                    } else {//UNSPECIFIED
                        childWidthSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
                    }
                    break;
                default://固定尺寸值
                    childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, EXACTLY);
                    break;
            }
        }

    }

這兩個條件:開發(fā)者的要求和自己的可用空間,開發(fā)者的要求在地位上是要絕對高于可用空間的,例如開發(fā)者寫了layout_width="48dp",那么就不必管你的內(nèi)部有沒有足夠的空間給子View用,直接限制子View的尺寸是48dp就好,也就是說mode是EXACTLY,而size是48dp所對應(yīng)的像素值;
對于每個子View,計算它的MeasureSpec,也就是尺寸限制的時候,依次查看它們的layout_width和layout_height這兩個屬性,分別用它們結(jié)合自己當前的可用寬度和可用高度,來計算出子View的限制;
xml文件里面的layout_width和layout_height,在java代碼里面會被轉(zhuǎn)換成View里的兩個屬性,在父View里調(diào)用子View的getLayoutParams()方法可以獲得一個LayoutParams對象,它包含了xml文件里的layout_打頭的參數(shù)的對應(yīng)值,其中它的widht和height這兩個屬性值就分別對應(yīng)了layout_width和layout_height的值,而且是轉(zhuǎn)換過了的值,它們在xml如果是wrap_content或者match_parent就會被分別轉(zhuǎn)換成WRAP_CONTENT和MATCH_PARENT這兩個常量,而如果它們在xml里面是具體的數(shù)值,是多少多少dp或者sp,那么width和height這兩個屬性里就是它們被轉(zhuǎn)換后的具體的像素值;通過LayoutParams的width和height這兩個屬性就可以得到開發(fā)者對子View的尺寸要求;利用它們結(jié)合自己的可用空間來計算出對子View的寬度和高度的限制;

對子View的測量是發(fā)生在父View(也就是自己)的onMeasure()方法里面的,所以這個時候自己的尺寸是還沒有確定的,你只能得到一個可用寬度或者高度,或者叫做可用空間,那么這個可用空間是怎么獲取的呢?這個可用空間,是從自己的onMeasure()方法的兩個參數(shù),也就是自己的寬度和高度限制里面獲得的,onMeasure()里的那兩個MeasureSpec,雖然只是一份限制,不能直接決定自己的尺寸,但依據(jù)這份限制自己可以得到一個可用空間,我還沒算出來自己多大,但是我可以知道我最多能有多大地方去給自己和子View用 ,這個最多是多少,要看MeasureSpec的mode,對于EXACTLY這種mode,你的可用寬度就是MeasureSpec里面的size;對于AT_MOST來說就是MeasureSpec的size,所以AT_MOST和EXACTLY在計算自己的可用空間的時候它們是完全一樣的,都是把自己的MeasureSpec里面的size拿來當成自己的可用空間,它們只是在測量完子View再測量自己的時候有區(qū)別;UNSPECIFIED這個mode表示自己的父View,這個layout的父View對自己沒有尺寸限制,也就是說自己的可用空間是無限的;

wrap_content,它除了讓子View自己測量之外,其實還有一個隱藏的限制條件,那就是不能超過父View的邊界,或者說要在父View的可用空間之內(nèi)

  1. 根據(jù)子View給出的尺寸,得出子View的位置,并保存它們的位置和尺寸;
    99%的情況每一個View它所測量的尺寸就是它的最終尺寸,在稍后要用的時候去調(diào)用getMeasuredWidth()和getMeasuredHeight()就行,為什么要保存它們呢?因為現(xiàn)在是測量階段,在接下來的布局階段,在onLayout()方法里面這些位置和尺寸才會被傳給子View,所以在這期間,需要把它們的值暫時保存,以備稍后使用;
    說明:1.不是所有的Layout都需要保存子View的位置(因為有的Layout可以在布局階段實時推導(dǎo)出子View的位置,例如LinearLayout);2.有時候?qū)δ承┳覸iew需要重復(fù)測量兩次或多次才能得到正確的尺寸和位置

  2. 根據(jù)子View的位置和尺寸計算出自己的尺寸,并用setMeasuredDimension()保存;
    根據(jù)子View的排布來就算出邊界,這個邊界就是你的尺寸了

2. 重寫onLayout()來擺放子View

調(diào)用每個子View的layout()方法,把之前在onMeasure()里保存下來的它們的位置和尺寸作為參數(shù)傳進去,讓它們把自己的位置和尺寸保存下來,并進行自我布局

image.png

觸摸反饋

  1. 重寫onTouchEvent(), 在里面寫上你的觸摸反饋算法,并返回true(關(guān)鍵是ACTION_DOWN事件時返回true);
  2. 如果是會發(fā)生觸摸沖突的ViewGroup,還需要重寫onInterceptTouchEvent(),在事件流開始時返回false,并在確認接管事件流時返回一次true,以實現(xiàn)對事件的攔截;
  3. 當子View臨時需要組織父View攔截事件流時,可以調(diào)用父View的requestDisallowInterceptTouchEvent(),通知父View在當前事件流中不再嘗試通過onInterceptTouchEvent()來攔截;

扔物線
HenCoder Plus

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言: 在接觸Android這么長時間,看到很多大牛都在和大家分享自己的知識,深有體會,剛好前段時間寫了一個Dem...
    楊艷偉閱讀 1,382評論 0 5
  • 系列文章之 Android中自定義View(一)系列文章之 Android中自定義View(二)系列文章之 And...
    YoungerDev閱讀 4,626評論 3 11
  • 系列文章之 Android中自定義View(一)系列文章之 Android中自定義View(二)系列文章之 And...
    YoungerDev閱讀 2,328評論 0 4
  • 【Android 自定義View之繪圖】 基礎(chǔ)圖形的繪制 一、Paint與Canvas 繪圖需要兩個工具,筆和紙。...
    Rtia閱讀 12,160評論 5 34
  • 告別一座城市 就如同風(fēng)告別了雨 來勢洶洶卻又蒼白無力 這里不是終點 我要用這雨滴與你告別 在下一個晝夜交替 要么洗...
    蝸汼閱讀 326評論 1 2

友情鏈接更多精彩內(nèi)容