自定義view之無所不能的path
最近項目中需要完成以下這個需求
UI給我了五張圖片,我感覺太浪費了,自定義view完全可以做而且適配起來更加的方便
最終實現效果
項目效果
擴展
需要知道技術點
在實現這個過程之前,我們需要了解path的一系列的原理(如果你瞭解path的用法直接跳過)
PathMeasure(是一個用來測量Path的類,主要有以下方法)
- setPath、 isClosed 和 getLength
這三個方法都如字面意思一樣,非常簡單,這裏就簡單是敘述一下,不再過多講解。
setPath 是 PathMeasure 與 Path 關聯的重要方法,效果和 構造函數 中兩個參數的作用是一樣的。
isClosed 用於判斷 Path 是否閉合,但是如果你在關聯 Path 的時候設置 forceClosed 爲 true 的話,這個方法的返回值則一定爲true。
getLength 用於獲取 Path 的總長度
- getSegment
//返回值(boolean) 判斷截取是否成功 true 表示截取成功,結果存入dst中,false 截取失敗,不會改變dst中內容
//startD 開始截取位置距離 Path 起點的長度 取值範圍: 0 <= startD < stopD <= Path總長度
//stopD 結束截取位置距離 Path 起點的長度 取值範圍: 0 <= startD < stopD <= Path總長度
//dst 截取的 Path 將會添加到 dst 中 注意: 是添加,而不是替換
//startWithMoveTo 起始點是否使用 moveTo 用於保證截取的 Path 第一個點位置不變
//如果 startD、stopD 的數值不在取值範圍 [0, getLength] 內,或者 startD == stopD 則返回值爲 false,不會改變 dst 內容。
//如果在安卓4.4或者之前的版本,在默認開啓硬件加速的情況下,更改 dst 的內容後可能繪製會出現問題,請關閉硬件加速或者給 dst 添加一個單個操作,例如: dst.rLineTo(0, 0)
boolean getSegment (float startD, float stopD, Path dst, boolean startWithMoveTo)
- getPosTan
/*這個方法是用於得到路徑上某一長度的位置以及該位置的正切值:
參數 作用 備註
返回值(boolean) 判斷獲取是否成功 true表示成功,數據會存入 pos 和 tan 中,
false 表示失敗,pos 和 tan 不會改變
distance 距離 Path 起點的長度 取值範圍: 0 <= distance <= getLength
pos 該點的座標值 座標值: (x==[0], y==[1])
tan 該點的正切值 正切值: (x==[0], y==[1])
*/
boolean getPosTan (float distance, float[] pos, float[] tan)
- getMatrix
這個方法是用於得到路徑上某一長度的位置以及該位置的正切值的矩陣:
/*
返回值(boolean) 判斷獲取是否成功 true表示成功,數據會存入matrix中,false 失敗,matrix內容不會改變
distance 距離 Path 起點的長度 取值範圍: 0 <= distance <= getLength
matrix 根據 falgs 封裝好的matrix 會根據 flags 的設置而存入不同的內容
flags 規定哪些內容會存入到matrix中 可選擇
POSITION_MATRIX_FLAG(位置)
ANGENT_MATRIX_FLAG(正切)
*/
boolean getMatrix (float distance, Matrix matrix, int flags)
實現
可以明顯的看出這個view的5個園的圓心都在一個大的圓上
通過path得到一個園,然後將圓分割5份
Path pathCircle = new Path();
pathCircle.addCircle(with / 2, hight / 2, hight / 2 - pading - radius, Path.Direction.CW);
通過PathMeasure的getPosTan方法得到等分點在圓上的座標,然後判斷當前的狀態,給選中的狀態圓不同的顏色值
float[] position = new float[2];
for (int index = 0; index < 5; index++) {
if (currentPosition == index) {
paint.setColor(Color.RED);
} else {
paint.setColor(Color.BLUE);
}
float allLength = pathMeasure.getLength();
distance = (allLength / 5) * (index + 1);
pathMeasure.getPosTan(distance, position, tan);
canvas.drawCircle(position[0], position[1], radius, paint);
}
實現完以後我們發現問題,圓的位置每個圓環的位置和效果圖不是一樣的,那是爲什麼呢?
其實在path添加大圓的時候我們只能控制path路徑的軌跡方向,並不能指定其開始位置,而且現在我們寫死了很多變量:顏色,圓環數等*
解決辦法:那我們用arc(圓弧)去畫指定其實位置;通過指定要屬性實現動態添加屬性;
優化
畫出圓弧,指定開始位置爲正上方及時-90°
Path pathCircle = new Path();
RectF rectF = new RectF(pading + radius, pading + radius, with - pading - radius, hight - pading - radius);
pathCircle.arcTo(rectF, -90, 359);
通過自定義屬性動態指定參數
// 寬
private int with;
// 高
private int hight;
// 間距
private int pading;
// 小圓環半徑
private int radius;
// 圓環寬度
private int paintWith;
// 圓環數
private int pie;
// 當前選中圓環
private int currentPosition;
// 正常顏色
private int normalColor;
// 選中顏色
private int clickColor;
// 畫筆
private Paint paint;
public ProgressCircleView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ProgressCircleOldView);
pading = a.getDimensionPixelOffset(R.styleable.ProgressCircleOldView_pading, 0);
radius = a.getDimensionPixelOffset(R.styleable.ProgressCircleOldView_radius, 10);
paintWith = a.getDimensionPixelOffset(R.styleable.ProgressCircleOldView_paintWith, 4);
pie = a.getInt(R.styleable.ProgressCircleOldView_pie, 5);
currentPosition = a.getInt(R.styleable.ProgressCircleOldView_currentPosition, 0);
normalColor = a.getColor(R.styleable.ProgressCircleOldView_normalColor, Color.BLUE);
clickColor = a.getColor(R.styleable.ProgressCircleOldView_clickColor, Color.RED);
a.recycle();
initPaint();
}
對應的xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ProgressCircleOldView">
<!--間距-->
<attr name="pading" format="dimension"/>
<!--小圓環半徑-->
<attr name="radius" format="dimension"/>
<!--圓環寬度-->
<attr name="paintWith" format="dimension"/>
<!--圓環數-->
<attr name="pie" format="integer"/>
<!--當前選中圓環-->
<attr name="currentPosition" format="integer"/>
<!--正常顏色-->
<attr name="normalColor" format="color"/>
<!-- 選中顏色-->
<attr name="clickColor" format="color"/>
</declare-styleable>
</resources>
得到座標點,畫出圓
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float[] position = new float[2];
float[] tan = new float[2];
float distance;
Path pathCircle = new Path();
RectF rectF = new RectF(pading + radius, pading + radius, with - pading - radius, hight - pading - radius);
pathCircle.arcTo(rectF, -90, 359);
PathMeasure pathMeasure = new PathMeasure(pathCircle, false);
for (int index = 0; index < pie; index++) {
if (currentPosition == index) {
paint.setColor(clickColor);
} else {
paint.setColor(normalColor);
}
float allLength = pathMeasure.getLength();
distance = (allLength / pie) * (index);
pathMeasure.getPosTan(distance, position, tan);
canvas.drawCircle(position[0], position[1], radius, paint);
}
}
到這裏我們基本已經完成了這個需求了但是估計大家還是沒有講PathMeasure沒有很好的理解,所以就有了下面的擴展
擴展
上面的效果在很多場景中我們都能用到,不如加載、經度顯示等;其實通過動畫我們也可以實現,但是自定義view也是可以的,而且它的效率更高,
靈活性更加好,功能也可以做的更加強大,主要是你實現起來還很簡單哦!
其實上面的矩形和圓軌跡都是走的同樣的邏輯,不過是path添加了不同的圖形,所以你可以自由發揮哦,所以就拿上面的圓形進度爲例子來講解了
path給定一個圖形
Path path = new Path();
path.addCircle(600, 400, 100, Path.Direction.CCW);
通過比getPosTan得到位置和偏移量
// 按照比例獲取
progress = progress < 1 ? progress + 0.0005 : 0;
Matrix matrix = new Matrix();
paint.setColor(Color.YELLOW);
measure.getPosTan((int) (measure.getLength() * progress), position, tan);
通過得到的點座標畫出箭頭
Path path1 = new Path();
path1.moveTo(position[0] - 20, position[1] + 20);
path1.lineTo(position[0], position[1]);
path1.lineTo(position[0] + 20, position[1] + 20);
// 是否閉合,閉合就是三角形了
path1.close();
通過tan得到箭頭的偏移量
Path path2 = new Path();
float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);
matrix.setRotate(degrees + 90, position[0], position[1]);
path2.addPath(path1, matrix);
通過getSegment得到進度上截取的弧線,鏈接箭頭
// 進度線
measure.getSegment(-1000, (int) (measure.getLength() * progress), path2, true);
paint.setColor(Color.BLUE);
canvas.drawPath(path2, paint);
最後不斷的刷新界面重畫
/**
* 繪製panth上每一個點的位置
* 帶箭頭的進度框
*
* @param canvas
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void PaintMatr(Canvas canvas) {
paint.setStrokeWidth(10);
paint.setStyle(Paint.Style.STROKE);
Path path = new Path();
path.addCircle(600, 400, 100, Path.Direction.CCW);
PathMeasure measure = new PathMeasure(path, false);
// 按照比例獲取
progress = progress < 1 ? progress + 0.0005 : 0;
Matrix matrix = new Matrix();
paint.setColor(Color.YELLOW);
measure.getPosTan((int) (measure.getLength() * progress), position, tan);
canvas.drawPath(path, paint);
// 箭頭
paint.setColor(Color.RED);
Path path1 = new Path();
path1.moveTo(position[0] - 20, position[1] + 20);
path1.lineTo(position[0], position[1]);
path1.lineTo(position[0] + 20, position[1] + 20);
// 是否閉合,閉合就是三角形了
path1.close();
Path path2 = new Path();
float degrees = (float) (Math.atan2(tan[1], tan[0]) * 180.0 / Math.PI);
matrix.setRotate(degrees + 90, position[0], position[1]);
path2.addPath(path1, matrix);
// 進度線
measure.getSegment(-1000, (int) (measure.getLength() * progress), path2, true);
paint.setColor(Color.BLUE);
canvas.drawPath(path2, paint);
invalidate();
}
到這裏你也是path就完事了 no no no其實path還能結合SVG( 是一種矢量圖,內部用的是 xml 格式化存儲方式存儲這操作和數據,你完全可以將 SVG 看作是 Path 的各項操作簡化書寫後的存儲格式)
svg和path的結合
SVG 是一種矢量圖,內部用的是 xml 格式化存儲方式存儲這操作和數據,你完全可以將 SVG 看作是 Path 的各項操作簡化書寫後的存儲格式
他們結合能創找出很多意想不到的東西,有興趣的同學可以自己去研究一下