1. 程式人生 > >android自定義View打造自己的專屬控制元件——風車控制元件

android自定義View打造自己的專屬控制元件——風車控制元件

Android 自定義View——打造自己的專屬控制元件

前段時間看到一個天氣應用,介面做的很好看。裡面一個風車轉動動畫和一個日出日落的動畫挺有意思的,於是自己也照著他的介面做了一個。
先看一下介面

自定義View一般有兩種情況

  1. 繼承自原有控制元件,擴充套件原有控制元件的功能。如前面介紹的自定義View實現圓角圖片
  2. 完全自定義控制元件,實現控制元件的測量,繪製等方法。打造自己的專屬控制元件。

這裡採用的是第二種方法,自己來繪製風車控制元件。
自定義控制元件需要繼承View,並實現onMeasure,onLayout,onDraw方法。從命名可以看出來這三個方法分別實現了控制元件的測量,定位和繪製。

先來說一下原理
程式碼主要工作在onDraw中實現。風車有下面的身體和上面的葉片組成,身體比較簡單,就是兩條成一定夾角的線,三個葉片成120度夾角,為了簡化計算,這裡每個葉片由兩個三角形組合,計算好每個頂點的座標之後用path來連線。上面比較麻煩的地方是每個頂點的計算,用到了三角函式,對於我這種大一高數學完之後就沒有碰過三角函式的人來說頂點的計算真是一種煎熬ORZ(每次繪製的不對了,sin cos換一下,或者加減換一下,差點開始懷疑人生了)。
風車繪製好之後我們還要讓它動起來,我們通過mAngel來控制角度,通過不斷的改變角度,呼叫invalidate來實現旋轉動畫。

下面我們來實現風車的繪製

public class WindMillView extends View {
    private static final int DEFAULT_COLOR = Color.WHITE;
    private static final int DEFAULT_WIDTH = 1;//畫筆寬度1dp
    private static final float LENGTH_1 = 5;//下面三角形高度5dp
    private static final float ALPHA = (float) (Math.PI / 6);//旋轉角度
    private static final
int DELAY = 30; private Paint mPaint; private Path mPath; private float mAngle = 0;//旋轉角度 通過改變角度實現旋轉動畫 public WindMillView(Context context) { this(context, null); } public WindMillView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public WindMillView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } //初始化 private void init() { mPaint = new Paint(); mPath = new Path(); mPaint.setColor(DEFAULT_COLOR); mPaint.setAntiAlias(true); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mAngle = (float) (mAngle + 3 * Math.PI / 360); float centerX = getWidth() / 2; float centerY = getHeight() * 4 / 9.0f; mPaint.setStrokeWidth(Utils.dp2px(getContext(), DEFAULT_WIDTH)); //繪製風車的身體 canvas.drawLine(centerX, centerY, centerX - getWidth() / 10, getHeight(), mPaint); canvas.drawLine(centerX, centerY, centerX + getWidth() / 10, getHeight(), mPaint); //繪製葉片 葉片由兩個三角形組成 length1是下面三角形的高 length為整個葉片長度 float length = (float) (Utils.dp2px(getContext(), LENGTH_1) * Math.sin(ALPHA) + getHeight() * 2 / 9.0f); float length1 = Utils.dp2px(getContext(), LENGTH_1); //分別計算葉片4個頂點的座標 通過path來繪製 float alpha = (float) (Math.PI / 2 - ALPHA + mAngle); mPath.moveTo(centerX, centerY); mPath.lineTo((float) (centerX + length1 * Math.cos(alpha)), (float) (centerY - length1 * Math.sin(alpha))); mPath.lineTo((float) (centerX + length * Math.cos(Math.PI / 2 + mAngle)), (float) (centerY - length * Math.sin(Math.PI / 2 + mAngle))); mPath.lineTo((float) (centerX + length1 * Math.cos(alpha + 2 * ALPHA)), (float) (centerY - length1 * Math.sin(alpha + 2 * ALPHA))); mPath.close(); canvas.drawPath(mPath, mPaint); //葉片之間夾角是2/3PI alpha = (float) (Math.PI / 2 - ALPHA + mAngle + Math.PI * 2 / 3); mPath.moveTo(centerX, centerY); mPath.lineTo((float) (centerX + length1 * Math.cos(alpha)), (float) (centerY - length1 * Math.sin(alpha))); mPath.lineTo((float) (centerX + length * Math.cos(Math.PI / 2 + mAngle + Math.PI * 2 / 3)), (float) (centerY - length * Math.sin(Math.PI / 2 + mAngle + Math.PI * 2 / 3))); mPath.lineTo((float) (centerX + length1 * Math.cos(alpha + 2 * ALPHA)), (float) (centerY - length1 * Math.sin(alpha + 2 * ALPHA))); mPath.close(); canvas.drawPath(mPath, mPaint); alpha = (float) (Math.PI / 2 - ALPHA + mAngle - Math.PI * 2 / 3); mPath.moveTo(centerX, centerY); mPath.lineTo((float) (centerX + length1 * Math.cos(alpha)), (float) (centerY - length1 * Math.sin(alpha))); mPath.lineTo((float) (centerX + length * Math.cos(Math.PI / 2 + mAngle - Math.PI * 2 / 3)), (float) (centerY - length * Math.sin(Math.PI / 2 + mAngle - Math.PI * 2 / 3))); mPath.lineTo((float) (centerX + length1 * Math.cos(alpha + 2 * ALPHA)), (float) (centerY - length1 * Math.sin(alpha + 2 * ALPHA))); mPath.close(); canvas.drawPath(mPath, mPaint); mPath.reset(); postInvalidateDelayed(DELAY); } }

上面的程式碼裡面所有的頂點都是由計算得到,整個過程看起來比較繁瑣,涉及到很多三角函式的計算,實際上,三個葉片是一樣的,只是繞著center旋轉了一定角度而已,所以,我們只要計算好其中一個葉片之後,其他的葉片頂點就是這個葉片的頂點繞center旋轉一定角度。
上述的計算中,我們在計算好一個葉片之後,通過一定旋轉角度,用三角函式來重新計算別的葉片的頂點座標,這對數學好的人應該是比較容易理解的,但是對與我這樣數學不好的人來說簡直要開始懷疑人生ORZ。好在Android中有更簡單的實現方法。剛才我們是畫布不動,通過旋轉座標來計算新的座標,那如果我們座標不動,旋轉一下畫布是不是也能達到這種效果。canvas中就提供了旋轉畫布的方法。

rotate(float degrees, float px, float py)
Preconcat the current matrix with the specified rotation.

canvas的rotate可以讓canvas繞一個點旋轉一定角度,所以,我們只需要計算出一個葉片的頂點座標之後再旋轉兩次就好了,是不是比剛才簡單了很多呢。

 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        mAngle = (float) (mAngle + 3 * Math.PI / 360);

        float centerX = getWidth() / 2;
        float centerY = getHeight() * 4 / 9.0f;
        mPaint.setStrokeWidth(Utils.dp2px(getContext(), DEFAULT_WIDTH));
        //繪製風車的身體
        canvas.drawLine(centerX, centerY, centerX - getWidth() / 10, getHeight(), mPaint);
        canvas.drawLine(centerX, centerY, centerX + getWidth() / 10, getHeight(), mPaint);

        //繪製葉片 葉片由兩個三角形組成 length1是下面三角形的高 length為整個葉片長度
        length = (float) (Utils.dp2px(getContext(), LENGTH_1) * Math.sin(ALPHA)
                + getHeight() * 2 / 9.0f);
        length1 = Utils.dp2px(getContext(), LENGTH_1);

        float alpha = (float) (Math.PI / 2 - ALPHA + mAngle);
        drawWindMill(canvas, centerX, centerY, alpha);

        canvas.save();
        canvas.rotate(120,centerX,centerY);
        drawWindMill(canvas, centerX, centerY, alpha);
        canvas.restore();

        canvas.save();
        canvas.rotate(240,centerX,centerY);
        drawWindMill(canvas, centerX, centerY, alpha);
        canvas.restore();

        mPath.reset();
        postInvalidateDelayed(DELAY);
    }

    private void drawWindMill(Canvas canvas, float centerX, float centerY, float alpha) {
        mPath.moveTo(centerX, centerY);
        mPath.lineTo((float) (centerX + length1 * Math.cos(alpha)),
                (float) (centerY - length1 * Math.sin(alpha)));

        mPath.lineTo((float) (centerX + length * Math.cos(Math.PI / 2 + mAngle)),
                (float) (centerY - length * Math.sin(Math.PI / 2 + mAngle)));

        mPath.lineTo((float) (centerX + length1 * Math.cos(alpha + 2 * ALPHA)),
                (float) (centerY - length1 * Math.sin(alpha + 2 * ALPHA)));
        mPath.close();
        canvas.drawPath(mPath, mPaint);
    }

除了旋轉之外,canvas還提供了平移,縮放等方法,可以大大減少計算。這裡要注意的是在進行變換之前先呼叫save方法儲存當前狀態,之後通過restore方法恢復。
好了,風車動畫就介紹到這裡,有什麼意見或者建議歡迎交流。