本文是學完HenCoder課程後的內容整理和心得體會(hencoder.com/)android
* 自定義繪製的方式是重寫繪製方法,其中最經常使用的是 onDraw()
* 繪製的關鍵是 Canvas 的使用
* Canvas 的繪製類方法: drawXXX() (關鍵參數:Paint)
* Canvas 的輔助類方法:範圍裁切(clipXXX()等)和幾何變換(Matrix)
* 可使用不一樣的繪製方法來控制遮蓋關係(繪製順序:背景、主體、子View、滑動邊緣漸變和滑動條、前景)
複製代碼
自定義繪製的四個級別(來源於HenCoder自定義View 1-1 繪製基礎):git
Canvas 的 drawXXX() 系列方法及 Paint 最多見的使用github
Canvas.drawXXX() 是自定義繪製最基本的操做。掌握了這些方法,你才知道怎麼繪製內容,例如怎麼畫圓、怎麼畫方、怎麼畫圖像和文字。組合繪製這些內容,再配合上 Paint 的一些常見方法來對繪製內容的顏色和風格進行簡單的配置,就可以應付大部分的繪製需求了。算法
Paint 的徹底攻略canvas
Paint 能夠作的事,不僅是設置顏色,也不僅是我在視頻裏講的實心空心、線條粗細、有沒有陰影,它能夠作的風格設置真的是很是多、很是細。api
Canvas 對繪製的輔助——範圍裁切和幾何變換。bash
大多數時候,它們並不會被用到,但一旦用到,一般都是很炫酷的效果。範圍裁切和幾何變換都是用於輔助的,它們自己並不酷,讓它們變酷的是設計師們的想象力與創造力。而你要作的,是把他們的想象力與創造力變成現實。ide
使用不一樣的繪製方法來控制繪製順序函數
控制繪製順序解決的並非「作不到」的問題,而是性能問題。一樣的一種效果,你不用繪製順序的控制每每也能作到,但須要用多個 View 甚至是多層 View 才能拼湊出來,所以代價是 UI 的性能;而使用繪製順序的控制的話,一個 View 就所有搞定了。工具
接下來總結一下上面四部分應用的api(來源於HenCoder自定義View 1-1 -- 1-5):
一大波知識點將要來臨:
前景提要:View的座標系
在 Android 裏,每一個 View 都有一個本身的座標系,彼此之間是不影響的。
這個座標系的原點是 View 左上角的那個點;水平方向是 x 軸,右正左負;
豎直方向是 y 軸,下正上負(注意,是下正上負,不是上正下負,和上學時候學的座標系方向不同)。
1.Canvas 的 drawXXX() 系列方法及 Paint 最多見的使用
Canvas 類下的全部 draw- 打頭的方法,例如 drawCircle() drawBitmap()。
Canvas.drawColor(@ColorInt int color) 顏色填充
drawCircle(float centerX, float centerY, float radius, Paint paint) 畫圓
前兩個參數 centerX centerY 是圓心的座標,第三個參數 radius 是圓的半徑,單位都是像素,
它們共同構成了這個圓的基本信息(即用這幾個信息能夠構建出一個肯定的圓);
第四個參數 paint我在視頻裏面已經說過了,它提供基本信息以外的全部風格信息,例如顏色、線條粗細、陰影等。
drawRect(float left, float top, float right, float bottom, Paint paint) 畫矩形
left, top, right, bottom 是矩形四條邊的座標。
drawPoint(float x, float y, Paint paint) 畫點
x 和 y 是點的座標。點的大小能夠經過 paint.setStrokeWidth(width) 來設置;
點的形狀能夠經過 paint.setStrokeCap(cap) 來設置:ROUND畫出來是圓形的點,SQUARE 或 BUTT 畫出來是方形的點。
drawOval(float left, float top, float right, float bottom, Paint paint) 畫橢圓
只能繪製橫着的或者豎着的橢圓,不能繪製斜的(斜的卻是也能夠,但不是直接使用 drawOval(),而是配合幾何變換,後面會講到)。
left, top, right, bottom 是這個橢圓的左、上、右、下四個邊界點的座標。
drawLine(float startX, float startY, float stopX, float stopY, Paint paint) 畫線
startX, startY, stopX, stopY 分別是線的起點和終點座標。
drawRoundRect(float left, float top, float right, float bottom, float rx, float ry, Paint paint) 畫圓角矩形
left, top, right, bottom 是四條邊的座標,rx 和 ry 是圓角的橫向半徑和縱向半徑。
drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint) 繪製弧形或扇形
drawArc() 是使用一個橢圓來描述弧形的。left, top, right, bottom 描述的是這個弧形所在的橢圓;
startAngle 是弧形的起始角度(x 軸的正向,即正右的方向,是 0 度的位置;順時針爲正角度,逆時針爲負角度),sweepAngle 是弧形劃過的角度;
useCenter 表示是否鏈接到圓心,若是不鏈接到圓心,就是弧形,若是鏈接到圓心,就是扇形。
drawPath(Path path, Paint paint) 畫自定義圖形
Path 能夠描述直線、二次曲線、三次曲線、圓、橢圓、弧形、矩形、圓角矩形。把這些圖形結合起來,就能夠描述出不少複雜的圖形。
Path 方法第一類:直接描述路徑。addXxx()——添加子圖形 或者 xxxTo()——畫線(直線或曲線)
addCircle(float x, float y, float radius, Direction dir) 添加圓
x, y, radius 這三個參數是圓的基本信息,最後一個參數 dir 是畫圓的路徑的方向。
addOval(float left, float top, float right, float bottom, Direction dir) / addOval(RectF oval, Direction dir) 添加橢圓
addRect(float left, float top, float right, float bottom, Direction dir) / addRect(RectF rect, Direction dir) 添加矩形
addRoundRect(RectF rect, float rx, float ry, Direction dir) / addRoundRect(float left, float top, float right, float bottom, float rx, float ry, Direction dir) / addRoundRect(RectF rect, float[] radii, Direction dir) / addRoundRect(float left, float top, float right, float bottom, float[] radii, Direction dir) 添加圓角矩形
addPath(Path path) 添加另外一個 Path
lineTo(float x, float y) / rLineTo(float x, float y) 畫直線
quadTo(float x1, float y1, float x2, float y2) / rQuadTo(float dx1, float dy1, float dx2, float dy2) 畫二次貝塞爾曲線
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) 畫三次貝塞爾曲線
moveTo(float x, float y) / rMoveTo(float x, float y) 移動到目標位置
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) 畫弧形
close() 封閉當前子圖形
Path 方法第二類:輔助的設置或計算,這類方法的使用場景比較少,例如:Path.setFillType(Path.FillType ft) 設置填充方式
drawBitmap(Bitmap bitmap, float left, float top, Paint paint) 畫 Bitmap
繪製 Bitmap 對象,也就是把這個 Bitmap 中的像素內容貼過來。其中 left 和 top 是要把 bitmap 繪製到的位置座標。
drawText(String text, float x, float y, Paint paint) 繪製文字
界面裏全部的顯示內容,都是繪製出來的,包括文字。 drawText() 這個方法就是用來繪製文字的。參數 text 是用來繪製的字符串,x 和 y 是繪製的起點座標。
Paint 類的幾個最經常使用的方法。具體是:
Paint.setStyle(Style style) 設置繪製模式
Paint.setColor(int color) 設置顏色
Paint.setStrokeWidth(float width) 設置線條寬度
Paint.setTextSize(float textSize) 設置文字大小
Paint.setAntiAlias(boolean aa) 設置抗鋸齒開關
2. Paint 的徹底攻略:Paint 的 API 大體能夠分爲 4 類:
顏色:
直接設置顏色:
setColor(int color)
setARGB(int a, int r, int g, int b)
設置 Shader:Paint.setShader(Shader shader)
LinearGradient 線性漸變
RadialGradient 輻射漸變
SweepGradient 掃描漸變
BitmapShader 用 Bitmap 來着色
跟 Canvas.drawBitmap() 同樣的效果。若是你想繪製圓形的 Bitmap,
就別用 drawBitmap() 了,改用 drawCircle() + BitmapShader就能夠了(其餘形狀同理)。
ComposeShader 混合着色器
設置顏色過濾:Paint.setColorFilter(ColorFilter filter)
LightingColorFilter 用來模擬簡單的光照效果的(改變R、G、B顏色值的比例多少)
PorterDuffColorFilter 是使用一個指定的顏色和一種指定的 PorterDuff.Mode 來與繪製對象進行合成。
ColorMatrixColorFilter 使用一個 ColorMatrix 來對顏色進行處理。 ColorMatrix 這個類,內部是一個 4x5 的矩陣,ColorMatrix 能夠把要繪製的像素進行轉換。
Paint.setXfermode(Xfermode xfermode):Xfermode 指的是你要繪製的內容和 Canvas 的目標位置的內容應該怎樣結合計算出最終的顏色。
但通俗地說,其實就是要你以繪製的內容做爲源圖像,以 View中已有的內容做爲目標圖像,選取一個 PorterDuff.Mode 做爲繪製內容的顏色處理方案。
PorterDuffXfermode Xfermode只有這一個子類
Xfermode 注意事項:
1. 使用離屏緩衝(Off-screen Buffer)
Canvas.saveLayer() 沒有特殊要求使用此方法,性能高
View.setLayerType()
效果:
setAntiAlias (boolean aa) 設置抗鋸齒
setStyle(Paint.Style style)
線條形狀:
setStrokeWidth(float width) 單位爲像素,默認值是 0,0和1的區別在於幾何變換
setStrokeCap(Paint.Cap cap) 設置線頭的形狀。線頭形狀有三種:BUTT 平頭、ROUND 圓頭、SQUARE 方頭。默認爲 BUTT。
setStrokeJoin(Paint.Join join) 設置拐角的形狀。有三個值能夠選擇:MITER 尖角、 BEVEL 平角和 ROUND 圓角。默認爲 MITER。
setStrokeMiter(float miter) 這個方法是對於 setStrokeJoin() 的一個補充,它用於設置 MITER 型拐角的延長線的最大值。
色彩優化:
setDither(boolean dither) 設置圖像的抖動。
setFilterBitmap(boolean filter) 設置是否使用雙線性過濾來繪製 Bitmap 。
setPathEffect(PathEffect effect) 使用 PathEffect 來給圖形的輪廓設置效果。對 Canvas 全部的圖形繪製有效
CornerPathEffect 把全部拐角變成圓角。
DiscretePathEffect 把線條進行隨機的偏離,讓輪廓變得亂七八糟。亂七八糟的方式和程度由參數決定。
DashPathEffect 使用虛線來繪製線條。
PathDashPathEffect 這個方法比 DashPathEffect 多一個前綴 Path ,因此顧名思義,它是使用一個 Path 來繪製「虛線」
SumPathEffect 這是一個組合效果類的 PathEffect 。它的行爲特別簡單,就是分別按照兩種 PathEffect 分別對目標進行繪製。
ComposePathEffect 這也是一個組合效果類的 PathEffect 。不過它是先對目標 Path 使用一個 PathEffect,而後再對這個改變後的 Path 使用另外一個 PathEffect。
setShadowLayer(float radius, float dx, float dy, int shadowColor) 在以後的繪製內容下面加一層陰影。
setMaskFilter(MaskFilter maskfilter) 爲以後的繪製設置 MaskFilter。
上一個方法 setShadowLayer() 是設置的在繪製層下方的附加效果;而這個 MaskFilter 和它相反,設置的是在繪製層上方的附加效果。
BlurMaskFilter(float radius, BlurMaskFilter.Blur style) 模糊效果的 MaskFilter。
NORMAL: 內外都模糊繪製
SOLID: 內部正常繪製,外部模糊
INNER: 內部模糊,外部不繪製
OUTER: 內部不繪製,外部模糊
EmbossMaskFilter 浮雕效果的 MaskFilter。
獲取繪製的 Path,例如:getFillPath(Path src, Path dst)
drawText() 相關 ,Paint 有些設置是文字繪製相關的,即和 drawText() 相關的。
drawText(String text, float x, float y, Paint paint)
drawTextOnPath() 沿着一條 Path 來繪製文字。
StaticLayout 繪製多行的文字,StaticLayout 並非一個 View 或者 ViewGroup ,而是 android.text.Layout 的子類,它是純粹用來繪製文字的。
StaticLayout 支持換行,它既能夠爲文字設置寬度上限來讓文字自動換行,也會在 \n 處主動換行。
StaticLayout(CharSequence source, TextPaint paint, int width, Layout.Alignment align, float spacingmult, float spacingadd, boolean includepad)
width 是文字區域的寬度,文字到達這個寬度後就會自動換行;
align 是文字的對齊方向;
spacingmult 是行間距的倍數,一般狀況下填 1 就好;
spacingadd 是行間距的額外增長值,一般狀況下填 0 就好;
includepad 是指是否在文字上下添加額外的空間,來避免某些太高的字符的繪製出現越界。
Paint 對文字繪製的輔助
設置顯示效果類:
setTextSize(float textSize) 設置文字大小。
setTypeface(Typeface typeface) 設置字體。
setFakeBoldText(boolean fakeBoldText) 是否使用僞粗體。
setStrikeThruText(boolean strikeThruText) 是否加刪除線。
setUnderlineText(boolean underlineText) 是否加下劃線。
setTextSkewX(float skewX) 設置文字橫向錯切角度,就是文字傾斜度。
setTextScaleX(float scaleX) 設置文字橫向放縮。也就是文字變胖變瘦。
setLetterSpacing(float letterSpacing) 設置字符間距。默認值是 0。
setFontFeatureSettings(String settings) 用 CSS 的 font-feature-settings 的方式來設置文字。
setTextAlign(Paint.Align align) 設置文字的對齊方式。一共有三個值:LEFT CETNER 和 RIGHT。默認值爲 LEFT。
setTextLocale(Locale locale) / setTextLocales(LocaleList locales) 設置繪製所使用的 Locale。
setHinting(int mode) 設置是否啓用字體的 hinting (字體微調)。
setSubpixelText(boolean subpixelText) 是否開啓次像素級的抗鋸齒( sub-pixel anti-aliasing )。
測量文字尺寸類:
float getFontSpacing() 獲取推薦的行距。
FontMetircs getFontMetrics() 獲取 Paint 的 FontMetrics。相似於英文的三格四線,字體在Android系統上會有5條線
FontMetrics是個相對專業的工具類,它提供了幾個文字排印方面的數值:ascent, descent, top, bottom, leading。
另外,ascent 和 descent 這兩個值還能夠經過 Paint.ascent() 和 Paint.descent() 來快捷獲取。
字體的高度可經過 descent - ascent 來獲取
getTextBounds(String text, int start, int end, Rect bounds) 獲取文字的顯示範圍。
參數裏,text 是要測量的文字,start 和 end 分別是文字的起始和結束位置,bounds 是存儲文字顯示範圍的對象,方法在測算完成以後會把結果寫進 bounds。
float measureText(String text) 測量文字的寬度並返回。
getTextWidths(String text, float[] widths) 獲取字符串中每一個字符的寬度,並把結果填入參數 widths。
光標相關:
getRunAdvance(CharSequence text, int start, int end, int contextStart, int contextEnd, boolean isRtl, int offset)
對於一段文字,計算出某個字符處光標的 x 座標。 start end 是文字的起始和結束座標;contextStart contextEnd 是上下文的起始和結束座標;isRtl 是文字的方向;offset 是字數的偏移,即計算第幾個字符處的光標。
getOffsetForAdvance(CharSequence text, int start, int end, int contextStart, int contextEnd, boolean isRtl, float advance)
給出一個位置的像素值,計算出文字中最接近這個位置的字符偏移量(即第幾個字符最接近這個座標)。
方法的參數很簡單: text 是要測量的文字;start end 是文字的起始和結束座標;contextStart contextEnd 是上下文的起始和結束座標;isRtl 是文字方向;advance 是給出的位置的像素值。填入參數,對應的字符偏移量將做爲返回值返回。
getOffsetForAdvance() 配合上 getRunAdvance() 一塊兒使用,就能夠實現「獲取用戶點擊處的文字座標」的需求。
hasGlyph(String string) 檢查指定的字符串中是不是一個單獨的字形 (glyph)。最簡單的狀況是,string 只有一個字母(好比 a)。
初始化:
reset() 重置 Paint 的全部屬性爲默認值。至關於從新 new 一個,不過性能固然高一些啦。
set(Paint src) 把 src 的全部屬性所有複製過來。至關於調用 src 全部的 get 方法,而後調用這個 Paint 的對應的 set 方法來設置它們。
setFlags(int flags) 批量設置 flags。至關於依次調用它們的 set 方法。
paint.setFlags(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
等價於 paint.setAntiAlias(true); 和 paint.setDither(true);
3. Canvas 對繪製的輔助——範圍裁切和幾何變換。
前景提要:
Canvas.save() 和 Canvas.restore() 及時保存和恢復繪製範圍,分別用在範圍裁切和幾何變換先後
範圍裁切:
Canvas.clipRect(left, top, right, bottom);
Canvas.clipPath()
幾何變換:
使用 Canvas 來作常見的二維變換(多個變換,該種方式是倒着執行的,Canvas 的幾何變換順序是反的)
Canvas.translate(float dx, float dy) 平移
參數裏的 dx 和 dy 表示橫向和縱向的位移。
Canvas.rotate(float degrees, float px, float py) 旋轉
參數裏的 degrees 是旋轉角度,單位是度(也就是一週有 360° 的那個單位),方向是順時針爲正向; px 和 py 是軸心的位置。
Canvas.scale(float sx, float sy, float px, float py) 放縮
參數裏的 sx sy 是橫向和縱向的放縮倍數; px py 是放縮的軸心。
Canvas.skew(float sx, float sy) 錯切
參數裏的 sx 和 sy 是 x 方向和 y 方向的錯切係數。
使用 Matrix 來作變換
使用 Matrix 來作常見變換(多個變換,順序執行)
建立 Matrix 對象;
調用 Matrix 的 pre/postTranslate/Rotate/Scale/Skew() 方法來設置幾何變換;
使用 Canvas.setMatrix(matrix) 或 Canvas.concat(matrix) 來把幾何變換應用到 Canvas。
使用 Matrix 來作自定義變換
Matrix.setPolyToPoly(float[] src, int srcIndex, float[] dst, int dstIndex, int pointCount) 用點對點映射的方式設置變換
setPolyToPoly() 的做用是經過多點的映射的方式來直接設置變換。「多點映射」的意思就是把指定的點移動到給出的位置,從而發生形變。
例如:(0, 0) -> (100, 100) 表示把 (0, 0) 位置的像素移動到 (100, 100) 的位置,這個是單點的映射,單點映射能夠實現平移。而多點的映射,就可讓繪製內容任意地扭曲。
使用 Camera 來作三維變換
Camera.rotate*() 三維旋轉
Camera.rotate*() 一共有四個方法: rotateX(deg) rotateY(deg) rotateZ(deg) rotate(x, y, z)
Camera.translate(float x, float y, float z) 移動
Camera.setLocation(x, y, z) 設置虛擬相機的位置
它的參數的單位不是像素,而是 inch,英寸。
Camera.setLocation(x, y, z) 的 x 和 y 參數通常不會改變,直接填 0 就好。
z值變大,相機後移,可以使幾何變換變大的圖標變小
4. 使用不一樣的繪製方法來控制繪製順序
drawBackground() 繪製背景,不容許重寫
onDraw() 繪製主體
寫在 super.onDraw() 的上面
若是把繪製代碼寫在 super.onDraw() 的上面,因爲繪製代碼會執行在原有內容的繪製以前,因此繪製的內容會被控件的原內容蓋住。
dispatchDraw():繪製子 View 的方法
寫在 super.dispatchDraw() 的下面
只要重寫 dispatchDraw(),並在 super.dispatchDraw() 的下面寫上你的繪製代碼,這段繪製代碼就會發生在子 View 的繪製以後,從而讓繪製內容蓋住子 View 了。
寫在 super.dispatchDraw() 的上面
把繪製代碼寫在 super.dispatchDraw() 的上面,這段繪製就會在 onDraw() 以後、 super.dispatchDraw() 以前發生,也就是繪製內容會出如今主體內容和子 View 之間。
onDrawForeground() API 23 才引入的,會依次繪製滑動邊緣漸變、滑動條和前景。
寫在 super.onDrawForeground() 的下面
若是你把繪製代碼寫在了 super.onDrawForeground() 的下面,繪製代碼會在滑動邊緣漸變、滑動條和前景以後被執行,那麼繪製內容將會蓋住滑動邊緣漸變、滑動條和前景。
寫在 super.onDrawForeground() 的上面
若是你把繪製代碼寫在了 super.onDrawForeground() 的上面,繪製內容就會在 dispatchDraw() 和 super.onDrawForeground() 之間執行,那麼繪製內容會蓋住子 View,但被滑動邊緣漸變、滑動條以及前景蓋住
draw() 總調度方法
寫在 super.draw() 的下面
因爲 draw() 是總調度方法,因此若是把繪製代碼寫在 super.draw() 的下面,那麼這段代碼會在其餘全部繪製完成以後再執行,也就是說,它的繪製內容會蓋住其餘的全部繪製內容。
寫在 super.draw() 的上面
同理,因爲 draw() 是總調度方法,因此若是把繪製代碼寫在 super.draw() 的上面,那麼這段代碼會在其餘全部繪製以前被執行,因此這部分繪製內容會被其餘全部的內容蓋住,包括背景。是的,背景也會蓋住它。
注意
關於繪製方法,有兩點須要注意一下:
1.出於效率的考慮,ViewGroup 默認會繞過 draw() 方法,換而直接執行 dispatchDraw(),以此來簡化繪製流程。因此若是你自定義了某個 ViewGroup 的子類(好比 LinearLayout)而且須要在它的除 dispatchDraw() 之外的任何一個繪製方法內繪製內容,你可能會須要調用 View.setWillNotDraw(false) 這行代碼來切換到完整的繪製流程(是「可能」而不是「必須」的緣由是,有些 ViewGroup 是已經調用過 setWillNotDraw(false) 了的,例如 ScrollView)。
2.有的時候,一段繪製代碼寫在不一樣的繪製方法中效果是同樣的,這時你能夠選一個本身喜歡或者習慣的繪製方法來重寫。但有一個例外:若是繪製代碼既能夠寫在 onDraw() 裏,也能夠寫在其餘繪製方法裏,那麼優先寫在 onDraw() ,由於 Android 有相關的優化,能夠在不須要重繪的時候自動跳過 onDraw() 的重複執行,以提高開發效率。享受這種優化的只有 onDraw() 一個方法。
複製代碼
具體效果和詳細描述,建議查看HenCoder 自定義View 1-1 到 1-5 節。
兩個階段:測量階段和佈局階段。
測量階段:從上到下遞歸地調用每一個 View 或者 ViewGroup 的 measure()方法,測量他們的尺寸並計算它們的位置;
佈局階段:從上到下遞歸地調用每一個 View 或者 ViewGroup 的 layout() 方法,把測得的它們的尺寸和位置賦值給它們。
View 或 ViewGroup 的佈局過程(來源於HenCoder自定義佈局 2-1 到 2-3)
佈局分類:
重寫 onMeasure() 來修改已有的 View 的尺寸;
重寫 onMeasure() 來全新定製自定義 View 的尺寸;
重寫 onMeasure() 方法,不調用 super.onMeasure(),徹底本身測量
重寫 onMeasure() 和 onLayout() 來全新定製自定義 ViewGroup 的內部佈局。
重寫 onMeasure() 的三個步驟:
1. 調用每一個子 View 的 measure() 來計算子 View 的尺寸
2.計算子 View 的位置並保存子 View 的位置和尺寸
3.計算本身的尺寸並用 setMeasuredDimension() 保存
複製代碼
重寫 onLayout() 的方式:
在 onLayout() 裏調用每一個子 View 的 layout() ,讓它們保存本身的位置和尺寸。
複製代碼
具體效果和詳細描述,建議查看HenCoder 自定義佈局 2-1 到 2-3 節,有視頻講解的哦。
onFinishInflate() 當View中全部的子控件均被映射成xml後觸發
onMeasure( int , int ) 肯定全部子元素的大小
onLayout( boolean , int , int , int , int ) 當View分配全部的子元素的大小和位置時觸發
onSizeChanged( int , int , int , int ) 當view的大小發生變化時觸發
onDraw(Canvas) view渲染內容的細節
onKeyDown( int , KeyEvent) 有按鍵按下後觸發
onKeyUp( int , KeyEvent) 有按鍵按下後彈起時觸發
onTrackballEvent(MotionEvent) 軌跡球事件
onTouchEvent(MotionEvent) 觸屏事件
onFocusChanged( boolean , int , Rect) 當View獲取或失去焦點時觸發
onWindowFocusChanged( boolean ) 當窗口包含的view獲取或失去焦點時觸發
onAttachedToWindow() 當view被附着到一個窗口時觸發
onDetachedFromWindow() 當view離開附着的窗口時觸發和onAttachedToWindow() 是相反的。
onWindowVisibilityChanged( int ) 當窗口中包含的可見的view發生變化時觸發
複製代碼
經過學習HenCoder的自定義View和佈局,也看了看HenCoder「仿寫酷界面」活動——徵稿中的幾個綜合練習,本身看懂了3個半,原諒我就看了一半的「小米運動首頁頂部的運動記錄界面」仿寫代碼。最後爲了更好的掌握和理解,本身嘗試着實現了一下「即刻的點贊效果」,本身仿寫的不太好,不過繪製和佈局仍是能夠講一講我本身的見解:
// 感興趣的同窗能夠看一看,請忽略動畫部分,哈哈哈,文字的動畫還沒作,縮放的動畫作的也不太好
package com.android.customwidget.widget;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.OvershootInterpolator;
import com.android.customwidget.R;
public class ThumbUpView extends View implements View.OnClickListener {
private Bitmap selected;
private Bitmap unselected;
private Bitmap shining;
private Paint paintIcon;
private Paint paintText;
private Paint paintCircle;
// 點贊數量
private int likeNumber;
// 圖標和文字間距
private int widthSpace;
private int textHeight;
// 文字的繪製是基於baseline的,而高度則是經過descent - ascent獲取的
private int textDescentAndBaselineSpace;
// 火花和點贊圖標之間的間距,此值爲負
private int shinAndThubSpace;
private Path mClipPath = new Path();
private float SCALE_MIN = 0.9f;
private float SCALE_MAX = 1f;
private float mScale = SCALE_MIN;
private float mUnScale = SCALE_MAX;
private int alpha;
private int alphaStart = 64;
private int alphaEnd = 0;
private float radius = 24;
private float radiusStart = 0;
private float radiusEnd;
// 是不是喜好
private boolean isLike = false;
public ThumbUpView(Context context) {
super(context);
init();
}
public ThumbUpView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public ThumbUpView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
likeNumber = 0;
widthSpace = dip2px(5);
shinAndThubSpace = -dip2px(8);
paintIcon = new Paint();
paintIcon.setStyle(Paint.Style.STROKE);
paintText = new Paint();
paintText.setAntiAlias(true);
paintText.setStyle(Paint.Style.STROKE);
paintText.setTextSize(dip2px(14));
paintText.setColor(getResources().getColor(R.color.comm_main_color));
paintCircle = new Paint();
paintCircle.setColor(Color.RED);
paintCircle.setAntiAlias(true);
paintCircle.setStyle(Paint.Style.STROKE);
paintCircle.setStrokeWidth(5);
// 獲取文字高度
// textHeight = (int) (paintText.descent() - paintText.ascent());
Paint.FontMetrics fontMetrics = paintText.getFontMetrics();
Paint.FontMetricsInt fm = paintText.getFontMetricsInt();
float ascent = fontMetrics.ascent;
float descent = fontMetrics.descent;
float top = fontMetrics.top;
float bottom = fontMetrics.bottom;
float leading = fontMetrics.leading;
textHeight = (int) (descent - ascent);
textDescentAndBaselineSpace = (int) (descent - leading);
selected = BitmapFactory.decodeResource(getResources(), R.drawable.ic_messages_like_selected);
unselected = BitmapFactory.decodeResource(getResources(), R.drawable.ic_messages_like_unselected);
shining = BitmapFactory.decodeResource(getResources(), R.drawable.ic_messages_like_selected_shining);
setOnClickListener(this);
}
/**
* 你們都說繼承自View的自定義控件須要重寫onMeasure方法,爲何?
* 其實若是咱們不重寫onMeasure方法,則父佈局就不知道你到底多大,
* 就會將其剩餘的全部空間都給你,此時若是還須要別的控件添加進父佈局,
* 則會出現沒有空間顯示該多餘出的控件,所以咱們須要本身測量咱們到底有多大
*
* 此處實際上須要根據widthMeasureSpec和heightMeasureSpec中的mode去分別設置寬高
* 不過自定義View的測量能夠根據本身的指望來設置
* 測量無非就是佈局中設置的padding值+你本身設定的間距+圖標寬高+文字寬高...
* */
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int measureWidth = getMeasureWidth();
int measureHeight = getMeasureHeight();
int i = resolveSize(measureWidth, widthMeasureSpec);
int j = resolveSize(measureHeight, heightMeasureSpec);
setMeasuredDimension(i, j);
}
// 根據mode和實際測量設置寬,本View未採用
private int getWidth(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
// 可用空間
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = specSize;
break;
case MeasureSpec.AT_MOST:
result = getMeasureWidth();
break;
case MeasureSpec.EXACTLY:
result = specSize;
/**
* 此處能夠不比較大小,由於用戶的須要大於一切,
* 那麼咱們會說若是咱們本身測量的寬大於上面的result(specSize)怎麼辦?那固然是出現Bug啦,
* 所以若加了 Math.max(getMeasureWidth(), result) 處理則會避免由用戶設置的過大而致使的Bug
* 不過雖然能夠避免用戶設置致使的Bug,可是可能須要開發此View的人依舊須要作相應的處理
* */
result = Math.max(getMeasureWidth(), result);
break;
}
return result;
}
// 根據mode和實際測量設置高,本View未採用
private int getHeight(int measureSpec) {
int result = 0;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = specSize;
break;
case MeasureSpec.AT_MOST:
result = getMeasureHeight();
break;
case MeasureSpec.EXACTLY:
result = specSize;
// 同理getWidth方法
result = Math.max(getMeasureHeight(), result);
break;
}
return result;
}
// 獲取測量的寬
private int getMeasureWidth() {
int widthResult = 0;
// 3 * widthSpace : 圖標左側、圖標與文字中間、文字右側都設置 5dp 間距
widthResult += selected.getWidth() + 3 * widthSpace + paintText.measureText(likeNumber + "");
// 必定不要忘記累加padding值
widthResult += getPaddingLeft() + getPaddingRight();
return widthResult;
}
// 獲取測量的高
private int getMeasureHeight() {
int heightResult = 0;
// 獲取點贊圖標以及點贊火花圖標組合後的高度
// , shinAndThubSpace 的緣由是兩圖標組合並不是是上下並列,而是火花會靠近點贊圖標
int iconHeight = selected.getHeight() + shining.getHeight() + shinAndThubSpace;
heightResult = Math.max(textHeight, iconHeight);
heightResult += getPaddingTop() + getPaddingBottom();
return heightResult;
}
// 周期函數--View大小發生改變時回調
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
Log.e("onSizeChanged", "onSizeChanged : width == " + w + " height == " + h + " oldw == " + oldw + " oldh == " + oldh);
radiusEnd = getCircleData()[2] + 3;
}
/**
* 繪製的位置,通常在測量時已經肯定好其位置
* 根據padding和本身設置的間距(沒有則爲0)以及想畫的位置肯定座標
* */
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawIcon(canvas);
drawNumber(canvas);
}
// 畫圖標
private void drawIcon(Canvas canvas) {
float left = widthSpace + getPaddingLeft();
float top = shining.getHeight() + getPaddingTop() + shinAndThubSpace;
Log.e("getMeasureWidth", "getMeasure == " + getMeasureWidth());
if (isLike) {
float shinLeft = left + selected.getWidth() / 2 - shining.getWidth() / 2;
float shinTop = getPaddingTop();
canvas.drawBitmap(shining, shinLeft, shinTop, paintIcon);
canvas.save();
canvas.scale(mScale, mScale);
canvas.drawBitmap(selected, left, top, paintIcon);
canvas.restore();
float[] circleData = getCircleData();
paintCircle.setAlpha(alpha);
canvas.drawCircle(circleData[0], circleData[1], radius, paintCircle);
} else {
canvas.save();
canvas.scale(mUnScale, mUnScale);
canvas.drawBitmap(unselected, left, top, paintIcon);
canvas.restore();
}
}
// 畫數字
private void drawNumber(Canvas canvas) {
Log.e("getMeasureHeight", "getMeasure == " + getMeasureHeight() + " " + textDescentAndBaselineSpace);
float left = selected.getWidth() + 2 * widthSpace + getPaddingLeft();
float top = shining.getHeight() + getPaddingTop() + shinAndThubSpace + selected.getHeight() / 2 + textHeight / 2 - textDescentAndBaselineSpace;
canvas.drawText(likeNumber + "", left, top, paintText);
}
// 獲取圓的信息-- 圓中心位置(座標)、和半徑
private float[] getCircleData() {
// 此圓最大要徹底包裹點贊圖標和火花圖標,所以其圓心Y座標要在點贊和火花圖標總體的中心
float centerX = getPaddingLeft() + widthSpace + selected.getWidth() / 2;
float iconHeight = shining.getHeight() + selected.getHeight() + shinAndThubSpace;
float centerY = getPaddingTop() + iconHeight / 2;
float iconWidthMax = Math.max(shining.getWidth(), selected.getWidth());
float radius = Math.max(iconWidthMax, iconHeight) / 2;
return new float[]{centerX, centerY,radius};
}
// --------------------------------Animate Start-------------------------------------
@Override
public void onClick(View v) {
if (isLike) {
likeNumber--;
showThumbDownAnim();
} else {
likeNumber++;
showThumbUpAnim();
}
}
private float getCircleRadiusAnim() {
return radius;
}
// 圓半徑大小動畫
public void setCircleRadiusAnim(float rudiusAnim) {
radius = rudiusAnim;
/**
* invalidate方法和postInvalidate方法都是用於進行View的刷新。
* invalidate方法應用在UI線程中,而postInvalidate方法應用在非UI線程中,
* 用於將線程切換到UI線程,postInvalidate方法最後調用的也是invalidate方法。
*/
invalidate(); // postInvalidate();
}
private int getCircleColorAnim() {
return alpha;
}
// 透明度動畫
public void setCircleColorAnim(int alphaAnim) {
alpha = alphaAnim;
invalidate();
}
public float getUnSelectAnim() {
return mUnScale;
}
// 取消點贊圖標縮放動畫
public void setUnSelectAnim(float scaleSize) {
mUnScale = scaleSize;
invalidate();
}
public float getSelectAnim() {
return mScale;
}
// 點贊圖標縮放動畫
public void setSelectAnim(float scaleSize) {
mScale = scaleSize;
invalidate();
}
/**
* 展現點贊動畫
* */
public void showThumbUpAnim() {
ObjectAnimator animator1 = ObjectAnimator.ofFloat(this, "selectAnim", SCALE_MIN, SCALE_MAX);
animator1.setDuration(150);
animator1.setInterpolator(new OvershootInterpolator());
ObjectAnimator animator2 = ObjectAnimator.ofFloat(this, "unSelectAnim", SCALE_MAX, SCALE_MIN);
animator2.setDuration(150);
animator2.addListener(new ClickAnimatorListener(){
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
isLike = true;
}
});
@SuppressLint("ObjectAnimatorBinding")
ObjectAnimator animator3 = ObjectAnimator.ofInt(this, "circleColorAnim", alphaStart, alphaEnd);
animator3.setInterpolator(new DecelerateInterpolator());
@SuppressLint("ObjectAnimatorBinding")
ObjectAnimator animator4 = ObjectAnimator.ofFloat(this, "circleRadiusAnim", radiusStart, radiusEnd);
animator4.setDuration(150);
AnimatorSet set = new AnimatorSet();
set.play(animator1).with(animator3).with(animator4);
set.play(animator1).after(animator2);
set.start();
}
/**
* 展現取消點贊動畫
* */
public void showThumbDownAnim() {
ObjectAnimator animator1 = ObjectAnimator.ofFloat(this, "selectAnim", SCALE_MAX, SCALE_MIN);
animator1.setDuration(150);
animator1.addListener(new ClickAnimatorListener(){
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
isLike = false;
}
});
ObjectAnimator animator2 = ObjectAnimator.ofFloat(this, "unSelectAnim", SCALE_MIN, SCALE_MAX);
animator2.setDuration(150);
animator2.setInterpolator(new OvershootInterpolator());
AnimatorSet set = new AnimatorSet();
set.play(animator2).before(animator1);
set.start();
}
/**
* 動畫監聽
* */
private abstract class ClickAnimatorListener extends AnimatorListenerAdapter {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
}
@Override
public void onAnimationCancel(Animator animation) {
super.onAnimationCancel(animation);
}
}
private int dip2px(float dpValue) {
final float scale = getContext().getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5f);
}
// --------------------------------Animate End---------------------------------------
}
複製代碼
ViewPropertyAnimator
使用方式:View.animate() 後跟 translationX() 等方法,動畫會自動執行。具體能夠跟的方法以及方法所對應的 View 中的實際操做的方法以下圖所示:
ObjectAnimator 使用方式:
1. 若是是自定義控件,須要添加 setter / getter 方法;
2. 用 ObjectAnimator.ofXXX() 建立 ObjectAnimator 對象;
3. 用 start() 方法執行動畫。
複製代碼
示例:
public class SportsView extends View {
float progress = 0;
......
// 建立 getter 方法
public float getProgress() {
return progress;
}
// 建立 setter 方法
public void setProgress(float progress) {
this.progress = progress;
// setter 方法記得加 invalidate()刷新繪製哦
invalidate();
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
......
canvas.drawArc(arcRectF, 135, progress * 2.7f, false, paint);
......
}
}
......
// 建立 ObjectAnimator 對象
ObjectAnimator animator = ObjectAnimator.ofFloat(view, "progress", 0, 65);
// 執行動畫
animator.start();
複製代碼
通用功能
setDuration(int duration) 設置動畫時長 單位是毫秒。
setInterpolator(Interpolator interpolator) 設置 Interpolator,Interpolator 其實就是速度設置器。你在參數裏填入不一樣的 Interpolator ,動畫就會以不一樣的速度模型來執行。
設置監聽器
ViewPropertyAnimator.setListener() / ObjectAnimator.addListener()
ViewPropertyAnimator.setUpdateListener() / ObjectAnimator.addUpdateListener()
ObjectAnimator.addPauseListener()
ViewPropertyAnimator.withStartAction/EndAction()
TypeEvaluator
關於 ObjectAnimator,上期講到能夠用 ofInt() 來作整數的屬性動畫和用 ofFloat() 來作小數的屬性動畫。
這兩種屬性類型是屬性動畫最經常使用的兩種,不過在實際的開發中,能夠作屬性動畫的類型仍是有其餘的一些類型。
當須要對其餘類型來作屬性動畫的時候,就須要用到 TypeEvaluator 了。
複製代碼
// 自定義 HslEvaluator
private class HsvEvaluator implements TypeEvaluator<Integer> {
float[] startHsv = new float[3];
float[] endHsv = new float[3];
float[] outHsv = new float[3];
@Override
public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
// 把 ARGB 轉換成 HSV
Color.colorToHSV(startValue, startHsv);
Color.colorToHSV(endValue, endHsv);
// 計算當前動畫完成度(fraction)所對應的顏色值
if (endHsv[0] - startHsv[0] > 180) {
endHsv[0] -= 360;
} else if (endHsv[0] - startHsv[0] < -180) {
endHsv[0] += 360;
}
outHsv[0] = startHsv[0] + (endHsv[0] - startHsv[0]) * fraction;
if (outHsv[0] > 360) {
outHsv[0] -= 360;
} else if (outHsv[0] < 0) {
outHsv[0] += 360;
}
outHsv[1] = startHsv[1] + (endHsv[1] - startHsv[1]) * fraction;
outHsv[2] = startHsv[2] + (endHsv[2] - startHsv[2]) * fraction;
// 計算當前動畫完成度(fraction)所對應的透明度
int alpha = startValue >> 24 + (int) ((endValue >> 24 - startValue >> 24) * fraction);
// 把 HSV 轉換回 ARGB 返回
return Color.HSVToColor(alpha, outHsv);
}
}
ObjectAnimator animator = ObjectAnimator.ofInt(view, "color", 0xff00ff00);
// 使用自定義的 HslEvaluator
animator.setEvaluator(new HsvEvaluator());
animator.start();
複製代碼
ofObject()藉助於 TypeEvaluator,屬性動畫就能夠經過 ofObject() 來對不限定類型的屬性作動畫了。
// 例如:
private class PointFEvaluator implements TypeEvaluator<PointF> {
PointF newPoint = new PointF();
@Override
public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
float x = startValue.x + (fraction * (endValue.x - startValue.x));
float y = startValue.y + (fraction * (endValue.y - startValue.y));
newPoint.set(x, y);
return newPoint;
}
}
ObjectAnimator animator = ObjectAnimator.ofObject(view, "position",
new PointFEvaluator(), new PointF(0, 0), new PointF(1, 1));
animator.start();
複製代碼
PropertyValuesHolder 同一個動畫中改變多個屬性
不少時候,你在同一個動畫中會須要改變多個屬性,例如在改變透明度的同時改變尺寸。若是使用 ViewPropertyAnimator,你能夠直接用連寫的方式來在一個動畫中同時改變多個屬性,而對於 ObjectAnimator,是不能這麼用的。不過你可使用 PropertyValuesHolder 來同時在一個動畫中改變多個屬性。
PropertyValuesHolder holder1 = PropertyValuesHolder.ofFloat("scaleX", 1);
PropertyValuesHolder holder2 = PropertyValuesHolder.ofFloat("scaleY", 1);
PropertyValuesHolder holder3 = PropertyValuesHolder.ofFloat("alpha", 1);
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view, holder1, holder2, holder3)
animator.start();
複製代碼
PropertyValuesHolder 的意思從名字能夠看出來,它是一個屬性值的批量存放地。因此你若是有多個屬性須要修改,能夠把它們放在不一樣的 PropertyValuesHolder 中,而後使用 ofPropertyValuesHolder() 統一放進 Animator。這樣你就不用爲每一個屬性單首創建一個 Animator 分別執行了。
AnimatorSet 多個動畫配合執行
有的時候,你不止須要在一個動畫中改變多個屬性,還會須要多個動畫配合工做,好比,在內容的大小從 0 放大到 100% 大小後開始移動。這種狀況使用 PropertyValuesHolder 是不行的,由於這些屬性若是放在同一個動畫中,須要共享動畫的開始時間、結束時間、Interpolator 等等一系列的設定,這樣就不能有前後次序地執行動畫了。這就須要用到 AnimatorSet 了。
ObjectAnimator animator1 = ObjectAnimator.ofFloat(...);
animator1.setInterpolator(new LinearInterpolator());
ObjectAnimator animator2 = ObjectAnimator.ofInt(...);
animator2.setInterpolator(new DecelerateInterpolator());
AnimatorSet animatorSet = new AnimatorSet();
// 兩個動畫依次執行
animatorSet.playSequentially(animator1, animator2);
animatorSet.start();
複製代碼
使用 playSequentially(),就可讓兩個動畫依次播放,而不用爲它們設置監聽器來手動爲他們監管協做。
AnimatorSet 還能夠這麼用:
// 兩個動畫同時執行
animatorSet.playTogether(animator1, animator2);
animatorSet.start();
// 使用 AnimatorSet.play(animatorA).with/before/after(animatorB)
// 的方式來精確配置各個 Animator 之間的關係
animatorSet.play(animator1).with(animator2);
animatorSet.play(animator1).before(animator2);
animatorSet.play(animator1).after(animator2);
animatorSet.start();
複製代碼
PropertyValuesHolders.ofKeyframe() 把同一個屬性拆分 除了合併多個屬性和調配多個動畫,你還能夠在 PropertyValuesHolder 的基礎上更進一步,經過設置 Keyframe (關鍵幀),把同一個動畫屬性拆分紅多個階段。例如,你可讓一個進度增長到 100% 後再「反彈」回來。
// 在 0% 處開始
Keyframe keyframe1 = Keyframe.ofFloat(0, 0);
// 時間通過 50% 的時候,動畫完成度 100%
Keyframe keyframe2 = Keyframe.ofFloat(0.5f, 100);
// 時間見過 100% 的時候,動畫完成度倒退到 80%,即反彈 20%
Keyframe keyframe3 = Keyframe.ofFloat(1, 80);
PropertyValuesHolder holder = PropertyValuesHolder.ofKeyframe("progress", keyframe1, keyframe2, keyframe3);
ObjectAnimator animator = ObjectAnimator.ofPropertyValuesHolder(view, holder);
animator.start();
複製代碼
ValueAnimator 最基本的輪子
不少時候,你用不到它,只是在你使用一些第三方庫的控件,而你想要作動畫的屬性卻沒有 setter / getter 方法的時候,會須要用到它。想具體瞭解的,能夠查看Android 屬性動畫:這是一篇很詳細的 屬性動畫 總結&攻略
自定義控件中,不免會遇到須要滑動的場景。而Canvas提供的scrollTo和scrollBy方法只能達到移動的效果,須要達到真正的滑動便須要兩把基礎利器Scroller和VelocityTracker。我我的並無在項目中實戰過,不過看的demo中會出現這兩個類,所以就瞭解了一下其基礎的API。
不過在瞭解Scroller和VelocityTracker以前咱們須要先了解幾點基礎:
getScrollX()、getScrollY()
View X軸的偏移量,是相對本身初始位置的滑動偏移距離,只有當有scroll事件發生時,這兩個方法纔能有值,不然getScrollX()、getScrollY()都是初始時的值0。
scrollTo()
是絕對滾動(以view的內容的中心爲原點,若是x爲負值,則向右滾,y爲負值向下滾),基於開始位置滾動,若是已經滾動到了指定位置,重複調用不起做用。
scrollBy()
是相對滾動,內部調用了scrollTo,它是基於當前位置的相對滑動;不基於開始位置,就意味着可重複調用有效果。
Scroller API: (滾動的一個封裝類)
構造方法
(1) Scroller(Context context) 建立一個 Scroller 實例。
參數解析:
第一個參數 context: 上下文;
複製代碼
(2) Scroller(Context context, Interpolator interpolator) 建立一個 Scroller 實例。
參數解析:
第一個參數 context: 上下文;
第二個參數 interpolator: 插值器,用於在 computeScrollOffset 方法中,而且是在 SCROLL_MODE 模式下,根據時間的推移計算位置。爲null時,使用默認 ViscousFluidInterpolator 插值器。
複製代碼
(3) Scroller(Context context, Interpolator interpolator, boolean flywheel)建立一個 Scroller 實例。
參數解析:
第一個參數 context: 上下文;
第二個參數 interpolator: 插值器,用於在 computeScrollOffset 方法中,而且是在 SCROLL_MODE 模式下,根據時間的推移計算位置。爲null時,使用默認 ViscousFluidInterpolator 插值器。
第三個參數 flywheel: 支持漸進式行爲,該參數只做用於 FLING_MODE 模式下。
複製代碼
經常使用公有方法
(1) setFriction(float friction) 用於設置在 FLING_MODE 模式下的摩擦係數
參數解析:
第一個參數 friction: 摩擦係數
複製代碼
(2) isFinished() 滾動是否已結束,用於判斷 Scroller
在滾動過程的狀態,咱們能夠作一些終止或繼續運行的邏輯分支。
複製代碼
(3) forceFinished(boolean finished) 強制的讓滾動狀態置爲咱們所設置的參數值 finished 。
(4) getDuration() 返回 Scroller 將持續的時間(以毫秒爲單位)。
(5) getCurrX() 返回滾動中的當前X相對於原點的偏移量。
(6) getCurrY() 返回滾動中的當前Y相對於原點的偏移量。
(7) getCurrVelocity() 獲取當前速度。
(8) computeScrollOffset() 計算滾動中的新座標,會配合着 getCurrX 和 getCurrY
方法使用,達到滾動效果。值得注意的是,若是返回true,說明動畫還未完成。
相反,返回false,說明動畫已經完成或是被終止了。
複製代碼
(9) startScroll public void startScroll(int startX, int startY, int dx, int dy) 經過提供起點,行程距離和滾動持續時間,進行滾動的一種方式,即 SCROLL_MODE。該方法能夠用於實現像ViewPager的滑動效果。
參數解析:
第一個參數 startX: 開始點的x座標
第二個參數 startY: 開始點的y座標
第三個參數 dx: 水平方向的偏移量,正數會將內容向左滾動。
第四個參數 dy: 垂直方向的偏移量,正數會將內容向上滾動。
第五個參數 duration: 滾動的時長
複製代碼
(10) fling public void fling(int startX, int startY, int velocityX, int velocityY,int minX, int maxX, int minY, int maxY) 用於帶速度的滑動,行進的距離將取決於投擲的初始速度。能夠用於實現相似 RecycleView 的滑動效果。
參數解析:
第一個參數 startX: 開始滑動點的x座標
第二個參數 startY: 開始滑動點的y座標
第三個參數 velocityX: 水平方向的初始速度,單位爲每秒多少像素(px/s)
第四個參數 velocityY: 垂直方向的初始速度,單位爲每秒多少像素(px/s)
第五個參數 minX: x座標最小的值,最後的結果不會低於這個值;
第六個參數 maxX: x座標最大的值,最後的結果不會超過這個值;
第七個參數 minY: y座標最小的值,最後的結果不會低於這個值;
第八個參數 maxY: y座標最大的值,最後的結果不會超過這個值;
值得一說:
minX <= 終止值的x座標 <= maxX
minY <= 終止值的y座標 <= maxY
複製代碼
(11) abortAnimation() public void abortAnimation() 中止動畫,值得注意的是,此時若是調用 getCurrX() 和 getCurrY() 移動到的是最終的座標,這一點和經過 forceFinished 直接將動畫中止是不相同的。
特別說明:getCurrX()和getCurrY() ,咱們從源碼看起
public class Scroller {
private int mStartX;//水平方向,滑動時的起點偏移座標
private int mStartY;//垂直方向,滑動時的起點偏移座標
private int mFinalX;//滑動完成後的偏移座標,水平方向
private int mFinalY;//滑動完成後的偏移座標,垂直方向
private int mCurrX;//滑動過程當中,根據消耗的時間計算出的當前的滑動偏移距離,水平方向
private int mCurrY;//滑動過程當中,根據消耗的時間計算出的當前的滑動偏移距離,垂直方向
private int mDuration; //本次滑動的動畫時間
private float mDeltaX;//滑動過程當中,在達到mFinalX前還須要滑動的距離,水平方向
private float mDeltaY;//滑動過程當中,在達到mFinalX前還須要滑動的距離,垂直方向
......
public void startScroll(int startX, int startY, int dx, int dy) {
startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
}
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
public void fling(int startX, int startY, int velocityX, int velocityY,
int minX, int maxX, int minY, int maxY) {
// Continue a scroll or fling in progress
if (mFlywheel && !mFinished) {
float oldVel = getCurrVelocity();
float dx = (float) (mFinalX - mStartX);
float dy = (float) (mFinalY - mStartY);
float hyp = (float) Math.hypot(dx, dy);
float ndx = dx / hyp;
float ndy = dy / hyp;
float oldVelocityX = ndx * oldVel;
float oldVelocityY = ndy * oldVel;
if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
Math.signum(velocityY) == Math.signum(oldVelocityY)) {
velocityX += oldVelocityX;
velocityY += oldVelocityY;
}
}
mMode = FLING_MODE;
mFinished = false;
float velocity = (float) Math.hypot(velocityX, velocityY);
mVelocity = velocity;
mDuration = getSplineFlingDuration(velocity);
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
float coeffX = velocity == 0 ? 1.0f : velocityX / velocity;
float coeffY = velocity == 0 ? 1.0f : velocityY / velocity;
double totalDistance = getSplineFlingDistance(velocity);
mDistance = (int) (totalDistance * Math.signum(velocity));
mMinX = minX;
mMaxX = maxX;
mMinY = minY;
mMaxY = maxY;
mFinalX = startX + (int) Math.round(totalDistance * coeffX);
// Pin to mMinX <= mFinalX <= mMaxX
mFinalX = Math.min(mFinalX, mMaxX);
mFinalX = Math.max(mFinalX, mMinX);
mFinalY = startY + (int) Math.round(totalDistance * coeffY);
// Pin to mMinY <= mFinalY <= mMaxY
mFinalY = Math.min(mFinalY, mMaxY);
mFinalY = Math.max(mFinalY, mMinY);
}
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
// 經過運算來計算mCurrX和mCurrY值
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
case FLING_MODE:
final float t = (float) timePassed / mDuration;
final int index = (int) (NB_SAMPLES * t);
float distanceCoef = 1.f;
float velocityCoef = 0.f;
if (index < NB_SAMPLES) {
final float t_inf = (float) index / NB_SAMPLES;
final float t_sup = (float) (index + 1) / NB_SAMPLES;
final float d_inf = SPLINE_POSITION[index];
final float d_sup = SPLINE_POSITION[index + 1];
velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
distanceCoef = d_inf + (t - t_inf) * velocityCoef;
}
mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
// 經過運算來計算mCurrX和mCurrY值
mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
// Pin to mMinX <= mCurrX <= mMaxX
mCurrX = Math.min(mCurrX, mMaxX);
mCurrX = Math.max(mCurrX, mMinX);
mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
// Pin to mMinY <= mCurrY <= mMaxY
mCurrY = Math.min(mCurrY, mMaxY);
mCurrY = Math.max(mCurrY, mMinY);
if (mCurrX == mFinalX && mCurrY == mFinalY) {
mFinished = true;
}
break;
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
......
複製代碼
從源碼中能夠看出getCurrX()和getCurrY()即mCurrX和mCurrY的值跟(mStartX、mStartY)和(mDeltaX、mDeltaY)/(velocityX、velocityY)有關:
左、上滑(mDeltaX、mDeltaY)/(velocityX、velocityY)值爲負,mCurrX和mCurrY從mStartX、mStartY初始值一點點減少
右、下滑(mDeltaX、mDeltaY)/(velocityX、velocityY)值爲正,mCurrX和mCurrY從mStartX、mStartY初始值一點點增大
複製代碼
由此能夠引伸出滑動中實時的偏移量的多少:
diffX = mStartX - mScroller.getCurrX();
diffY = mStartY - mScroller.getCurrY();
複製代碼
VelocityTracker API:(滑動速度跟蹤器VelocityTracker, 用來監聽手指移動改變的速度;)
(1) obtain() 獲取一個 VelocityTracker 對象。VelocityTracker的構造函數是私有的,也就是不能經過new來建立。
(2) recycle() 回收 VelocityTracker 實例。
(3) clear() 重置 VelocityTracker 回其初始狀態。
(4) addMovement(MotionEvent event) 爲 VelocityTracker 傳入觸摸事件(包括ACTION_DOWN、ACTION_MOVE、ACTION_UP等),這樣 VelocityTracker 才能在調用了 computeCurrentVelocity 方法後,正確的得到當前的速度。
(5) computeCurrentVelocity(int units) 根據已經傳入的觸摸事件計算出當前的速度,能夠經過getXVelocity 或 getYVelocity進行獲取對應方向上的速度。值得注意的是,計算出的速度值不超過Float.MAX_VALUE。
參數解析:
參數 units: 速度的單位。值爲1表示每毫秒像素數,1000表示每秒像素數。
複製代碼
(6) computeCurrentVelocity(int units, float maxVelocity) 根據已經傳入的觸摸事件計算出當前的速度,能夠經過getXVelocity 或 getYVelocity進行獲取對應方向上的速度。值得注意的是,計算出的速度值不超過maxVelocity。
參數解析:
第一個參數 units: 速度的單位。值爲1表示每毫秒像素數,1000表示每秒像素數。
第二個參數 maxVelocity: 最大的速度,計算出的速度不會超過這個值。值得注意的是,這個參數必須是正數,且其單位就是咱們在第一參數設置的單位。
複製代碼
(7) getXVelocity() 獲取最後計算的水平方向速度,使用此方法前須要記得先調用computeCurrentVelocity
(8) getYVelocity() 獲取最後計算的垂直方向速度,使用此方法前須要記得先調用computeCurrentVelocity
(9) getXVelocity(int id) 獲取對應的手指id最後計算的水平方向速度,使用此方法前須要記得先調用computeCurrentVelocity
參數解析:
參數 id: 觸碰的手指的id
複製代碼
(10) getYVelocity(int id) 獲取對應的手指id最後計算的垂直方向速度,使用此方法前須要記得先調用computeCurrentVelocity
參數解析:
參數 id: 觸碰的手指的id
複製代碼
小結:
VelocityTracker 的 API 簡單明瞭,咱們能夠用記住一個套路。
ViewConfiguration API:
獲取實例: ViewConfiguration viewConfiguration = ViewConfiguration.get(Context);
經常使用對象方法:(主要關注前3個方法)
// 獲取touchSlop (系統 滑動距離的最小值,大於該值能夠認爲滑動)
int touchSlop = viewConfiguration.getScaledTouchSlop();
// 得到容許執行fling (拋)的最小速度值
int minimumVelocity = viewConfiguration.getScaledMinimumFlingVelocity();
// 得到容許執行fling (拋)的最大速度值
int maximumVelocity = viewConfiguration.getScaledMaximumFlingVelocity();
// Report if the device has a permanent menu key available to the user
// (報告設備是否有用戶可找到的永久的菜單按鍵)
// 即判斷設備是否有返回、主頁、菜單鍵等實體按鍵(非虛擬按鍵)
boolean hasPermanentMenuKey = viewConfiguration.hasPermanentMenuKey();
複製代碼
經常使用靜態方法:
// 得到敲擊超時時間,若是在此時間內沒有移動,則認爲是一次點擊
int tapTimeout = ViewConfiguration.getTapTimeout();
// 雙擊間隔時間,在該時間內被認爲是雙擊
int doubleTapTimeout = ViewConfiguration.getDoubleTapTimeout();
// 長按時間,超過此時間就認爲是長按
int longPressTimeout = ViewConfiguration.getLongPressTimeout();
// 重複按鍵間隔時間
int repeatTimeout = ViewConfiguration.getKeyRepeatTimeout();
複製代碼
引伸:OverScroll簡介:
在Android手機上,當咱們滾動屏幕內容到達內容邊界時,若是再滾動就會有一個發光效果。
並且界面會進行滾動一小段距離以後再回復原位,這些效果是如何實現的呢?
咱們須要使用Scroller和scrollTo的升級版OverScroller和overScrollBy了,還有發光的EdgeEffect類。
複製代碼
因爲我此前並無研究過OverScroll和EdgeEffect類,因此就不列舉其API進行闡述啦,感興趣的小夥伴本身瞭解下吧。
自定義觸摸反饋的關鍵:
1. 重寫 onTouchEvent(),在裏面寫上你的觸摸反饋算法,並返回 true(關鍵是 ACTION_DOWN 事件時返回 true)。
2. 若是是會發生觸摸衝突的 ViewGroup,還須要重寫 onInterceptTouchEvent(),
在事件流開始時返回 false,並在確認接管事件流時返回一次 true,以實現對事件的攔截。
3. 當子 View 臨時須要組織父 View 攔截事件流時,能夠調用父 View 的 requestDisallowInterceptTouchEvent() ,
通知父 View 在當前事件流中再也不嘗試經過 onInterceptTouchEvent() 來攔截。
複製代碼
特別說明: 本文大部分直接複製於HenCoder中的內容(算是用於整合自定義控件知識點),少部分本身的理解和實戰。
心得體會:因爲最近在項目中遇到了日常控件沒法實現,並且github上也沒搜索到的效果,所以引發了我從新撿起曾經研究過屢次,但都放棄了的自定義View,重新跟着凱哥(扔物線:朱凱大佬)的HenCoder走了一遍知識點,而且也作了其練習項目,可是當這些都作完後,感受本身仍是隻知其一不知其二,這才引起我本身練習一個自定義控件的想法。開始着手去仿寫「即刻的點贊效果」,固然一開始是很頭疼的,畢竟歷來沒有本身動手寫過(只改過現成的),一開始在計算該View的大小時就出錯不斷,致使畫的位置也不對,可是當我靜下心來一點一點想本身究竟須要什麼樣子,而且經過固定一個圖標的位置,來肯定其它圖標和文字的位置時,才感受豁然開朗,就這樣一點點調試終於畫出了相似於原View的效果(固然畫的不是很好,還有一些細節已經考慮大小改變之類的,沒有動手嘗試,動畫作的也不是很好,不過最重要的仍是真正知道了到底如何測量和繪製)。因此,建議跟我以前同樣的小夥伴,必定要動手嘗試的計算繪製一個本身的View,收穫確定會有的。哈哈哈,囉嗦的夠多啦,本文到此結束啦,我仍是個小菜鳥,繼續努力啦。
......
(注:如有什麼地方闡述有誤,敬請指正。)