自定義view步驟:
- 獲取xml里設(shè)置的顏色,寬度等資源
- 初始化畫筆paint
- 重寫onDraw.(還有其他繪制方法)
- canvas.drawXXX
- 進階:
- 暴露方法, 通過postInvalidate()重繪.
ps: 重繪切記開子線程
低版本的適配用 new RectF(坐標(biāo)X...), 再傳入使用的draw方法即可
path類簡述
詳細請移步HenCoder1-1
path 的方法分為兩種, 直接描述路徑和輔助的設(shè)置/計算, path預(yù)先畫完草稿后最后用canvas.drawPath來繪制
- Path
- 直接描述路徑
- 添加子圖形, addXXX()
- addCircle(); 添加圓
- addArc; 強制無痕跡到新起點的arcTo, 畫弧形
- addOval(); 添加橢圓
- addRect(); 添加矩形
- addRoundRect(); 添加圓角矩形
- addPath(); 添加另一個path
- 畫線, XXXTo()
- lineTo(); 畫線
- arcTo(); 畫弧形
- close(); 封閉子圖形(有痕跡的回到最初起點)
- 添加子圖形, addXXX()
- 輔助的設(shè)置或計算,
- moveTo(); 位移起點
- 直接描述路徑
path 在繪制 Rect 時無法設(shè)置起點, 強制默認起點左上角, 可設(shè)置繪制方向順時針/逆時針
path 在繪制 RoundCirdle 時可以設(shè)置.
View 中方法執(zhí)行順序
- onFinishInflate
- onMeasure
- onSizeChanged
- onLayout
- onDraw
invalidate 和 requestLayout 區(qū)別
invalidate(): 只調(diào)用 onDraw();
requestLayout(): 會調(diào)用 onMeasure(), onLayout(), 不一定調(diào)用onDraw()
各個Animator區(qū)別
ViewPropertyAnimator: 限制于使用View自帶的屬性
ObjectAnimator: 可用于自定義控件的自定義屬性
Animator 中只應(yīng)做取插值的操作, 繪制/getPosTan 等方法應(yīng)該放在 onDraw里
何時獲取寬高
View 的生命周期和 Activity 的 onCreate, onStart, onResume 不同步. 如果此時還在測量, 得到的height, width 可能為0.
- onWindowFocusChanged(). 注意: Activity 窗口獲取/失去焦點時會被頻繁調(diào)用
- view.post(runnable)
- ViewTreeObserver, 注意: onGlobalLayout 也會被多次調(diào)用
- view.measure, 需根據(jù) LayoutParams 劃分:
- match_parent: 無法獲取, 理論上無法知道此刻 parentSize;
- 具體數(shù)值:
- wrap_content
畫一個仿 mastodon 的轉(zhuǎn)發(fā)按鈕
目標(biāo)
- 效果地址:mastodon殆知閣的貓站主頁 https://mao.daizhige.org/web/getting-started
- 效果gif:
- image
分析
- image
- 使用 Chrome 的 More Tools -> Animations記錄動畫, 并用10%的速度播放后發(fā)現(xiàn). 按鈕可以拆分為兩部分, 不動的圓角矩形和以圓角矩形作為軌跡移動的兩個三角形
- image
- image
- 三角形移動的動畫是一個在Animations分析下, 于450ms就超過了一半, 目測是一個減速的動畫
- 實現(xiàn)方案 android 自定義 View.
獲取參數(shù)
android 中 View 繪制時是設(shè)置的 View 整體的寬高
截圖一楨并在 PS 下放大到 2000% 后如圖獲取到參數(shù)(單位: 格)
- image
- 圓角矩形包含邊寬時, 寬30, 高24, 邊寬6.
- 三角形邊寬不另算, 底邊18, 高 12. 伸出圓角矩形的部分為6.(PS: 此處邊寬是否計入會在后面解釋)
- 所以 View 整體的寬為圓角矩形的寬 + 左右兩側(cè)三角形伸出圓角矩形的部分 = 30 + 6 * 2 = 42, 高為圓角矩形的高 + 上下兩側(cè)(三角形的高 - 圓角矩形的邊寬) = 24 + (12 - 6) * 2 = 36.
- 三角形與圓角矩形交接處寬度為2.
- 三角形與圓角矩形的1/2高度處的距離為1
繪制 View
android 中 View 繪制通過繼承 View 后重寫
onDraw方法.onDraw中,canvas理解為畫布/畫紙,paint理解為畫筆
android 中畫筆 paint 分為兩種, STORKE 和 FILL, 直譯就是一筆和畫滿, 具體的解釋是如果指定的區(qū)域是閉合的, 用 FILL 會把這個區(qū)域填滿. 而 STROKE只會畫邊框
- 首先把坐標(biāo)系從默認的左上角移動到 View 的中心.
canvas.translate(mWidth/2, mHeight/2);//坐標(biāo)系原點切到控件1/2處
圓角矩形的繪制只要邊框即可, 不需要填滿 使用的是 STROKE, 又因為
canvas.drawRoundRect(...)方法會把畫筆的起點扯到圓角矩形的左上角, 無法實現(xiàn)三角形在圓角矩形的左側(cè)中間位置開始移動, 所以最終的繪畫方案是通過依次繪制四個頂點來畫出圓角矩形, 使用canvas.drawPath. paint 的寬度為6三角形的繪制, 因為需要填滿三角形內(nèi)部, 所以用的是 FILL. 繪制方法同樣是
canvas.drawPath- image
透明邊
此時三角形也畫出來了, 但是原效果的三角形的邊好像是外邊框透明了? 那好, 來從畫筆 paint 里找設(shè)置外邊框的筆, 找了一圈。。。。。。emmm 沒有.
那換一個角度, 并沒有什么三角形的外邊框, 而是在三角形的上面, 有一條透明的線.
好的, 那畫好了透明色的線來看, 還是原來的樣子.
因為 android 的繪制原則是覆蓋, 所以一個區(qū)域如果有圖案了,只能去覆蓋它, 但是透明色的下面是原來的顏色, 所以覆蓋上去了并不能透明.
所以看似透明邊. 實際是背景色邊而已.
但是為了保證三角形的圓角不會因誤差被擦除, 需要將三角形和背景色邊兩個path重疊起來, 并且重疊出保留三角形. 用到了 PorterDuffXfermode 的 DST_OVER, 保留了作為的DST三角形
- image
動畫 Animation
android 中動畫的概念可以拆分為, 用一個插值器拿到 此刻進度 在動畫總進度的百分比 + 此刻 的 canvas 上畫了啥. 插值器可以理解為動畫執(zhí)行到了哪一刻
舉個例子, 一條用 path 畫的直線本身在一次onDraw內(nèi)就可以直接完成, 現(xiàn)在把它的 path 路徑用一個 1000 毫秒的動畫來完成, 過程是: 默認的每10ms獲取一次 此刻 插值器的值, 同時調(diào)用postInvalidate()方法觸發(fā)onDraw來繪制,onDraw里根據(jù)插值器的值獲取到執(zhí)行到了總進度的多少, 來決定怎么畫.
- 根據(jù)分析, 我們直接使用 DecelerateInterpolator 這個減速插值器即可
- 讓三角形沿著圓角矩形滾動可以看作讓三角形沿著繪制圓角矩形時的 path 路徑滾動.
- 通過
mPathMeasure.setPath(path, true)綁定 path, -
onDraw時不斷調(diào)用mPathMeasure.getPosTan方法來得到, 此刻插值器的值 * 總進度走到的點的坐標(biāo), 以及趨勢方向與x軸的夾腳
- 通過
問題
此時發(fā)現(xiàn), 三角形一直保持著繪制時的標(biāo)準(zhǔn)坐標(biāo)系, 并沒有按照預(yù)想的旋轉(zhuǎn).
旋轉(zhuǎn)三角形
我們需要在左側(cè)邊時的三角形逆時針旋轉(zhuǎn)90度, 在有側(cè)邊時的三角形順時針旋轉(zhuǎn)90度, 也就是-90 和 90.
根據(jù)動畫 Animation 中提到的 mPathMeasure.getPosTan(distance, pos[], tan[])方法, 參數(shù)依次是, 此刻的進度, 此刻的點坐標(biāo)數(shù)組pos[] 以及此刻的切線值. 這里通過不詳細解釋, 直接通過 float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI), 拿到趨勢方向與x軸的夾腳, 將畫布旋轉(zhuǎn)即可canvas.rotate(degrees), 同理畫出右側(cè)三角形(此處可以展開講講, 但考慮到篇幅, 可根據(jù)注釋理解)
- image
矩形改成圓角矩形
之前繪制的是矩形, 而圓角可以用path.arcTo(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo方法繪制. 如圖的圓弧是占地 left, top 到 right, bottom 的圓的 -180 度開始劃過了 90 度的部分
其中,只畫left, top, right, bottom 確定的橢圓為

這次的view為

于是效果從
- [圖片上傳失敗...(image-90462c-1523629514385)]
轉(zhuǎn)換成了 - image
最終效果

優(yōu)化
1.顏色
根據(jù)"數(shù)碼去色計"的"顯示原生值"設(shè)置獲取按鈕的各種顏色
2.透明三角替換透明邊
當(dāng)三角形的圓角數(shù)值固定且view很小時, 三角形會矮于view導(dǎo)致三角形頂部多處一個"小角"
- 解決方案 將透明邊改成透明三角形, 確保透明部分始終蓋住底部矩形
3.添加點擊邏輯
3.1.縮放
view 自帶了 animate(). 直接調(diào)用來縮放整體.
- ACTION_DOWN變小
- ACTION_UP變回
- ACTION_CANCEL變回
3.2. 當(dāng)手指滑出View時
普通的父布局不會管子view的 touch 事件, 但是 RecyclerView 會在 onInterceptTouchEvent 里判斷當(dāng) event 區(qū)域超過子 view 后調(diào)用 setScrollStat(), 攔截掉接下來的 move 事件. 這樣子 view 被ACTION_CANCEL, 父布局 RecyclerView 根據(jù) move 來滑動
如果手指超出了 view 后發(fā)生位移(Recyclerview中), 可根據(jù) Recyclerview 觸發(fā)的 ACTION_CANCEL 獲取到離開的標(biāo)志
如果 view 無法位移(在普通 view 中), 只可用 onTouchEvent 根據(jù) ACTION_UP 拿到手指離開的標(biāo)志
優(yōu)化后的效果
-
image
參考
感謝黃海奇的指點
Hencoder1-1
arcToMethod
getBackgroundColor
Path測量工具:PathMeasure
安卓自定義View進階-PathMeasure
CSDN | 自定義view系列(3)--給自定義View添加點擊事件
TODO
Android自定義View長按事件的實現(xiàn)









