編輯:關於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; } }
曾經讓人煩透的數學還是有點用處的。
通過widget定義,我們在widget列表中看到了我們的TestWidget,當我們拖拽widget到主頁時,如果在appwidet-provider中定義了andr
在項目中遇到需要提供給用戶一個密碼輸入框明文/密文切換顯示的需求,在網上搜索一圈都沒有發現完整的實現,幸而找到了一個實現的思路。先上效果圖,看了錄制屏幕gif的教程,無奈
最近在項目開發中,由於項目的需求要實現一些列表的單選,多選,全選,批量輸入之類的功能,其實功能的實現倒不是很復雜,需求中也沒有涉及到復雜的動畫什麼之類,主要是解決列表數據
CoordinatorLayout 實現了多種Material Design中提到的滾動效果。目前這個框架提供了幾種不用寫動畫代碼就能工作的方法,這些效果包括: *讓浮動