編輯:關於Android編程
這裡展示的View估計項目中多半是用不到的,只是用來加深理解的。文章末尾會有全部的代碼,如果想研究可以復制過去直接運行,不需要額外的資源。
先看效果:

這裡指針是通過手指來改變方向的,並不能通過數字參數來改變,如果需要,可以更改相應的代碼。
理論的涉及也非常簡單,如下所示:
在坐標系中,一個點與原點連線與X軸的正切值 tan = 點的縱坐標 ÷ 點的橫坐標 在每一個象限中,正切函數是單調函數;如圖所示:

繪制上圖有多種方法,首先介紹一種簡單的方法:
將線段旋轉多個角度,這樣可以繪制出一個圓弧型:
private int width;
private int height;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.translate(width / 2, height / 2);
Paint mPaint = new Paint();
mPaint.setStrokeWidth(5);
for (int i = 0; i <= 360; i += 5) { // 繪制圓形之間的連接線
canvas.drawLine(0, 120, 0, 200, mPaint);
canvas.rotate(10);
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
width = w;
height = h;
}
上述代碼的執行效果:

雖然丑了點,但可以說明問題。不過這樣做,我們缺乏對弧形的控制。例如:如何實現圓弧呢?是不是要手動計算起始坐標,旋轉的角度,掃過的角度等各種各樣的問題?因為怕麻煩,這個方案就被我華麗的拋棄了。

思路:
從圓心發射一條射線出來,與兩個圓相交於點A和點B,鏈接A與B,就可以劃出一條我們想要的線段。 均勻的發射多條射線,我們就可以得到一個由線段組成的圓弧。 如果兩個圓圈是圓弧的話,就可以達到我們所要的效果所以,最終確定的步驟為:
畫一個大圓弧 畫一個縮小版的小圓弧 均勻地在兩個圓弧之間畫線段用動態圖來展示下:

相關代碼比較多,在文章末尾已經貼出來了(88-172行,代碼中有後續的細節處理,需要甄別下相關的代碼),這裡只是寫下思路,不再重復貼代碼了

這步要實現的效果如上圖所示
假設,之前的指針為OZ,現在我們用手指觸摸了點A,這時我們希望指針變為OB,那麼,該如何實現呢?
獲取A點的坐標(通過onTouchEvent()可以獲取到) 畫取線段OA(O點為(0,0),所以可以畫取) 通過測量OA,可以利用PathMeasure.getPosTan()來獲取B點的坐標(指針的長度是固定的) 在Cavas中畫OB線段
如果我們觸摸點為X,距離過短怎麼辦呢?
鏈接OX,並用MeasurePath來測量OX的長度,以及X的坐標(a,b) 計算OY與OX的比例 R = OY ÷ OX 計算Y點的坐標 x = a × R, y = b × R 在Cavas中畫OY線段

上圖情況是我們不想看到。如果指針偏到最右邊,就不能再往下偏了;左邊同理。這個時候,就需要想到tan函數的性質:
在每一個象限中,正切函數是單調函數

說明下:
在第二象限中,當前的tan值小於邊界OA的tan值a時,說明此時是在邊界外面;如果大於a,說明在邊界裡面 在第一象限中,當前的tan值大於OB的tan值b時,說明在邊界外面;如果小於b,說明在邊界裡面
知道上述知識後,就非常好處理,具體過程如下:
在繪制弧形時,記錄下左側邊界的tan值和右側邊界的tan值(下面代碼129-151行) 在第一象限和第二象限時,記錄下當前位置的tan值,並與邊界的tan值進行比較來判斷是否在邊界中:(下面代碼214-233行)再具體的細節我在後面的代碼中由詳細的注釋,各位可以看看
/**
* Created by Kevin on 2016/8/31.
*
* 需要費腦的地方:
* 1.繪制多條線段組成弧形
* 2.指針跟隨著手指方向且長度確定
* 3.指針的指向不能越過儀表盤
*/
public class LinearCircle extends View {
private int width;
private int height;
private Paint outerCirclePaint;//外層圓的畫筆
private Paint innerCirclePaint;//內層圓的畫筆
private Paint linePaint;//線段畫筆
private Paint arrowPaint;//指針畫筆
private Path outerCirclePath;//外層圓的Path
private Path innerCirclePath;//內層圓的Path
private Path linePath;//線段的Path
private Path arrowPath;//指針的Path
private Path measureArrowPath;//arrowPath借助該Path來保持一定的長度
private RectF outRectF;//用於繪制外層圓
private RectF innerRectF;//用於繪制內層圓
private int count = 80;//畫count根線
private static int outerR = 200;//外部圓環的半徑
private static int innerR = (int) (200 * 0.618f);//內部圓環的半徑
private int shortageAngle = 140;//缺失的部分的角度
private int startAngle;//開始的角度
private int sweepAngle;//掃過的角度
private float[] leftEndPoint;//左側邊界的坐標
private float[] rightEndPoint;//右側邊界的坐標
private float leftEndTan;//左側邊界的tan值
private float rightEndTan;//右側邊界的tan值
private float nowX = 0;//觸摸位置的橫坐標
private float nowY = 0;//觸摸位置的縱坐標
private static float percent = 0.9f;//指針與內層圓的比值
private float arrowLength = innerR * percent;//指針的長度
private PathMeasure arrowMeasure;//用於指針的測量
public LinearCircle(Context context) {
super(context);
initPaint();
initAngle();
}
public LinearCircle(Context context, AttributeSet attrs) {
super(context, attrs);
initPaint();
initAngle();
}
public LinearCircle(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaint();
initAngle();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
width = w;
height = h;
//讓指針一開始指向正上方
nowX = 0;
nowY = -1;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.translate(width / 2, height / 2);
drawOuterCircle();
drawInnerCircle();
drawLine(canvas);
drawArrow(canvas);
}
/**
* 外層圓圈
*/
private void drawOuterCircle() {
//一般繪制圓圈的方法,不做介紹了
outerCirclePath = new Path();
if (outRectF == null) {
outRectF = new RectF(-outerR, -outerR, outerR, outerR);
}
outerCirclePath.addArc(outRectF, startAngle, sweepAngle);
}
/**
* 內層圓圈
*/
private void drawInnerCircle() {
//一般繪制圓圈的方法,不做介紹了
innerCirclePath = new Path();
if (innerRectF == null) {
innerRectF = new RectF(-innerR, -innerR, innerR, innerR);
}
innerCirclePath.addArc(innerRectF, startAngle, sweepAngle);
}
/**
* 畫直線,組成一個類似於弧形的形狀
*
* @param canvas
*/
private void drawLine(Canvas canvas) {
linePath = new Path();
//用於外層圓的測量
PathMeasure outMeasure = new PathMeasure(outerCirclePath, false);
float outlength = outMeasure.getLength();
float[] outPos = new float[2];
//用於內層圓的測量
PathMeasure inMeasure = new PathMeasure(innerCirclePath, false);
float inlength = inMeasure.getLength();
float[] inPos = new float[2];
//確定左側末尾的坐標以及tan值
if (leftEndPoint == null) {
leftEndPoint = new float[2];
//通過getPosTan拿到內層圓的左側末尾坐標
inMeasure.getPosTan(0, leftEndPoint, null);
//因為指針要短一點;所以x,y都乘以percent才是指針真正的左側末尾坐標
leftEndPoint[0] = leftEndPoint[0] * percent;
leftEndPoint[1] = leftEndPoint[1] * percent;
//確定指針在左側末尾時的tan值
leftEndTan = leftEndPoint[1] / leftEndPoint[0];
}
//確定右側末尾的坐標以及tan值
if (rightEndPoint == null) {
rightEndPoint = new float[2];
//通過getPosTan拿到內層圓的右側末尾坐標
inMeasure.getPosTan(inlength, rightEndPoint, null);
//因為指針要短一點;所以x,y都乘以percent才是指針真正的右側末尾坐標
rightEndPoint[0] = rightEndPoint[0] * percent;
rightEndPoint[1] = rightEndPoint[1] * percent;
//確定指針在右側末尾時的tan值
rightEndTan = rightEndPoint[1] / rightEndPoint[0];
}
//用來畫多條線段,組成弧形
for (int i = 0; i <= count; i++) {
//外層圓當前的弧長
float outNowLength = outlength * i / (count * 1.0f);
//當前弧長下對應的坐標outPos
outMeasure.getPosTan(outNowLength, outPos, null);
//內層圓當前的弧長
float inNowLength = inlength * i / (count * 1.0f);
//當前弧長下對應的坐標inPos
inMeasure.getPosTan(inNowLength, inPos, null);
//moveTo到內層圓弧上的點
linePath.moveTo(outPos[0], outPos[1]);
//lineTo到外層圓弧上的點
linePath.lineTo(inPos[0], inPos[1]);
canvas.drawPath(linePath, linePaint);
}
}
/**
* 繪制指針
*
* @param canvas
*/
private void drawArrow(Canvas canvas) {
//measureArrowPath只專門用來做計算的,不繪制(當然也可以不用多創建這個對象,直接用arrowPath來完成測量,繪制工作;
//這裡是為了任務單一,做了區分)
measureArrowPath = new Path();
//指針最終是由arrowPath來繪制的
arrowPath = new Path();
arrowPath.reset();
measureArrowPath.reset();
//用來封裝指針的末尾坐標
float[] endPoint = new float[2];
//指針的起始坐標為原點,也就是(0,0)
measureArrowPath.moveTo(0, 0);
//指向手指目前的位置
measureArrowPath.lineTo(nowX, nowY);
//arrowMeasure用來測量原點到手指位置的線段
arrowMeasure = new PathMeasure(measureArrowPath, false);
//觸摸位置與原點的長度
float nowLineLength = arrowMeasure.getLength();
//距離原點過近(也就是長度不夠長)的處理
if (nowLineLength < arrowLength) {
//計算需要擴大的倍數(固定長度 ÷ 當前長度)
float expand = arrowLength / (nowLineLength);
//重置數據,並測量新數據
measureArrowPath.reset();
measureArrowPath.moveTo(0, 0);
measureArrowPath.lineTo(nowX * expand, nowY * expand);
arrowMeasure = new PathMeasure(measureArrowPath, false);
}
//測量指針末尾的坐標(指針在measureArrowPath這條線段上,且小於等於measureArrowPath線段的長度;
// 通過getPosTan()來確定線段在長度為arrowLength時的坐標位置)
arrowMeasure.getPosTan(arrowLength, endPoint, null);
//第一象限的處理
if (endPoint[0] > 0 && endPoint[1] > 0) {
//右下角的情況處理
double nowTan = endPoint[1] / endPoint[0];
//當前觸摸位置的tan值大於邊界的tan值,表示手指目前在左側邊界的下方
if (nowTan > rightEndTan) {
endPoint[0] = rightEndPoint[0];
endPoint[1] = rightEndPoint[1];
}
}
//第二象限的處理
if (endPoint[0] < 0 && endPoint[1] > 1) {
//左下角的情況處理
double nowTan = endPoint[1] / endPoint[0];
//當前觸摸位置的tan值小於邊界的tan值,表示手指目前在右側邊界的下方
if (nowTan < leftEndTan) {
endPoint[0] = leftEndPoint[0];
endPoint[1] = leftEndPoint[1];
}
}
//這裡默認了第三、第四現象一般沒有限制;如果圓弧的缺口過大,需要處理下;方式與上面的相似
//這時,指針的末尾位置最終確定了,可以繪制了
arrowPath.moveTo(0, 0);
arrowPath.lineTo(endPoint[0], endPoint[1]);
canvas.drawPath(arrowPath, arrowPaint);
}
//通過觸摸等事件改變指針的指向
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
case MotionEvent.ACTION_UP:
nowX = event.getX();
nowY = event.getY();
break;
}
//nowX和nowY是以左上角為原點的坐標系,這裡進行了平移
nowX = nowX - width / 2;
nowY = nowY - height / 2;
invalidate();
return true;
}
/**
* 初始化畫筆
*/
private void initPaint() {
if (outerCirclePaint == null) {
outerCirclePaint = new Paint();
outerCirclePaint.setStyle(Paint.Style.STROKE);
outerCirclePaint.setColor(Color.BLACK);
}
if (innerCirclePaint == null) {
innerCirclePaint = new Paint();
innerCirclePaint.setStyle(Paint.Style.STROKE);
outerCirclePaint.setColor(Color.BLACK);
}
if (linePaint == null) {
linePaint = new Paint();
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setStrokeWidth(4);
linePaint.setColor(0xff1d8ffe);
}
if (arrowPaint == null) {
arrowPaint = new Paint();
arrowPaint.setStyle(Paint.Style.FILL_AND_STROKE);
arrowPaint.setColor(Color.RED);
arrowPaint.setStrokeWidth(4);
}
}
/**
* 根據shortageAngle來調整圓弧的角度
*/
private void initAngle() {
sweepAngle = 360 - shortageAngle;
startAngle = 90 + shortageAngle / 2;
}
}
曾經讓人煩透的數學還是有點用處的。
Android學習筆記:Home Screen Widgets(2):關於Widget
通過widget定義,我們在widget列表中看到了我們的TestWidget,當我們拖拽widget到主頁時,如果在appwidet-provider中定義了andr
Android 實現密碼輸入框動態明文/密文切換顯示效果
在項目中遇到需要提供給用戶一個密碼輸入框明文/密文切換顯示的需求,在網上搜索一圈都沒有發現完整的實現,幸而找到了一個實現的思路。先上效果圖,看了錄制屏幕gif的教程,無奈
android listview ExpandableListView實現多選,單選,全選,edittext實現批量輸入
最近在項目開發中,由於項目的需求要實現一些列表的單選,多選,全選,批量輸入之類的功能,其實功能的實現倒不是很復雜,需求中也沒有涉及到復雜的動畫什麼之類,主要是解決列表數據
Android 5.0中CoordinatorLayout的使用技巧
CoordinatorLayout 實現了多種Material Design中提到的滾動效果。目前這個框架提供了幾種不用寫動畫代碼就能工作的方法,這些效果包括: *讓浮動