1. 程式人生 > >自定義View之實現日出日落太陽動效

自定義View之實現日出日落太陽動效

    以前也很羨慕網上大神隨手寫寫就是一個很漂亮的自定義控制元件,所以我下決心也要學著去寫,剛好最近複習了Android View的繪製流程知識,看來看去就是那些個知識點,沒點產出總感覺很迷。現在個人呢用的是華為榮耀8手機,碰巧在看自帶的天氣APP時,滑到最下面看到那個動效圖:日出時間和日落時間上邊是一個半圓,白天任意的時刻(在日出和日落時間之間)都有對應一個太陽從日出時刻沿著半圓弧做動畫特效,個人第一感覺就是:就拿這個來練練手啦!於是拿著筆和紙,畫了模型圖,甚至求什麼sin、cos函式,有點過分了哈,還得溫習下三角函式。。。好,話不多說,先一睹為快:

   

        程式碼傳送門


    實現思路:

        1:首先需要繪製封閉的半圓,對應知識點:在自定義的onDraw()中拿canvas繪製;

        2:其次是要設定日出和日落的時間以及當前的時間,這個必須是可以從外面配置的;

        3:計算 日落時間 - 日出時間 的總分鐘數,這裡稱為:totalMinute,再計算 當前時間 - 日出時間 的總分鐘數,這裡稱為:currentMinute拿 currentMinute/totalMinute 計算得到 的資料保留2位小數,然後再乘以180,就能得到當前時間需要旋轉的角度,因為我們畫的是半圓,弧度就是180°;

        4:當前時間的角度我們在第三步拿到了,那麼:假如我們得到的當前時間對應的角度是60°,我們的動畫就需要從0°到60°過渡執行(實際在Android上來講,這個0°對應的是起始角度180°,為了方便描述這裡假設日出對應的點在0°),在執行的過程中我們必須拿到這個60°的半圓上對應的x,y座標點,方便我們在invalidate()更新view的時候,把小太陽不斷的繪製在0~60度這個圓弧上;

        5:根據第4步,將角度從0°不斷地升到60°,在這其中,我們需要不斷的拿每一度所對應的x,y座標,然後把小太陽圖片draw在這個位置上,因為我們知道圓的半徑radius,也知道角度,角度區間是【0~60】,這個時候回去找找我們的高中數學老師,老師會告訴我們三角函式sin和cos函式,直接計算得到每一度所對應的點離圓弧底部和圓心垂直方向的絕對距離,最後算出當前角度對應的x,y座標,為了方便理解,我也不知道這圖怎麼畫,直接手繪了一幅,湊合看吧:

        因為中間圓心的座標我們已知,角度和半徑已知,通過sin求的Y的絕對值,通過cos求得X的絕對值,然後用圓心的座標減去求得X,Y,最終得到圓弧上各個點的座標;

        6:到了最後一步,我們直接使用動畫過渡下就好了,具體的看原始碼去吧。。。

    原始碼實現

         思路有了,那就擼起袖子擼程式碼唄:原始碼方面的就不說太多了,也沒啥好講的,上面給了傳送門,註釋寫的很清楚了,我就直接貼一下自定義view的程式碼:

        太陽圖片:

       

        SunAnimationView.java:

public class SunAnimationView extends View
{

    private int mWidth; //螢幕寬度
    private int marginTop = 20;//離頂部的高度
    private int mCircleColor;  //圓弧顏色
    private int mFontColor;  //字型顏色
    private int mRadius;  //圓的半徑

    private float mCurrentAngle; //當前旋轉的角度
    private float mTotalMinute; //總時間(日落時間減去日出時間的總分鐘數)
    private float mNeedMinute; //當前時間減去日出時間後的總分鐘數
    private float mPercentage; //根據所給的時間算出來的百分佔比
    private float positionX, positionY; //太陽圖片的x、y座標
    private float mFontSize;  //字型顏色

    private String mStartTime; //開始時間(日出時間)
    private String mEndTime; //結束時間(日落時間)
    private String mCurrentTime; //當前時間

    private Paint mPaint; //畫筆
    private RectF mRectF; //半圓弧所在的矩形
    private Bitmap mSunIcon; //太陽圖片
    private WindowManager wm;

    public SunAnimationView(Context context)
    {
        this(context, null);
    }

    public SunAnimationView(Context context, @Nullable AttributeSet attrs)
    {
        this(context, attrs, 0);
    }

    public SunAnimationView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)
    {
        super(context, attrs, defStyleAttr);
        initView(context, attrs);
    }

    private void initView(Context context, @Nullable AttributeSet attrs)
    {

        TypedArray type = context.obtainStyledAttributes(attrs, R.styleable.SunAnimationView);
        mCircleColor = type.getColor(R.styleable.SunAnimationView_sun_circle_color, getContext().getResources().getColor(R.color.text_black_two));
        mFontColor = type.getColor(R.styleable.SunAnimationView_sun_font_color, getContext().getResources().getColor(R.color.text_black_three));
        mRadius = type.getInteger(R.styleable.SunAnimationView_sun_circle_radius, DisplayUtils.dp2px(getContext(), 150));
        mRadius = DisplayUtils.dp2px(getContext(), mRadius);
        mFontSize = type.getDimension(R.styleable.SunAnimationView_sun_font_size, DisplayUtils.dp2px(getContext(), 12));
        mFontSize = DisplayUtils.dp2px(getContext(), mFontSize);
        type.recycle();

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mSunIcon = BitmapFactory.decodeResource(getResources(), R.drawable.icon_sun);

    }

    public void setTimes(String startTime, String endTime, String currentTime)
    {
        mStartTime = startTime;
        mEndTime = endTime;
        mCurrentTime = currentTime;

        mTotalMinute = calculateTime(mStartTime, mEndTime);//計算總時間,單位:分鐘
        mNeedMinute = calculateTime(mStartTime, mCurrentTime);//計算當前所給的時間 單位:分鐘
        mPercentage = Float.parseFloat(formatTime(mTotalMinute, mNeedMinute));//當前時間的總分鐘數佔日出日落總分鐘數的百分比
        mCurrentAngle = 180 * mPercentage;

        setAnimation(0, mCurrentAngle, 5000);

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    {
        wm = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
        mWidth = wm.getDefaultDisplay().getWidth();
        positionX = mWidth / 2 - mRadius - 20; // 太陽圖片的初始x座標
        positionY = mRadius; // 太陽圖片的初始y座標
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom)
    {
        super.onLayout(changed, mWidth / 2 - mRadius, marginTop, mWidth / 2 + mRadius, mRadius * 2 + marginTop);
    }

    @Override
    protected void onDraw(Canvas canvas)
    {
        //第一步:畫半圓
        drawSemicircle(canvas);
        canvas.save();

        //第二步:繪製太陽的初始位置 以及 後面在動畫中不斷的更新太陽的X,Y座標來改變太陽圖片在檢視中的顯示
        drawSunPosition(canvas);

        //第三部:繪製圖上的文字
        drawFont(canvas);

        super.onDraw(canvas);
    }

    /**
     * 繪製半圓
     */
    private void drawSemicircle(Canvas canvas)
    {
        mRectF = new RectF(mWidth / 2 - mRadius, marginTop, mWidth / 2 + mRadius, mRadius * 2 + marginTop);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setDither(true);//防止抖動
        mPaint.setColor(mCircleColor);
        canvas.drawArc(mRectF, 180, 180, true, mPaint);
    }

    /**
     * 繪製太陽的位置
     */
    private void drawSunPosition(Canvas canvas)
    {
        canvas.drawBitmap(mSunIcon, positionX, positionY, mPaint);
    }

    /**
     * 繪製底部左右邊的日出時間和日落時間
     *
     * @param canvas
     */
    private void drawFont(Canvas canvas)
    {
        mPaint.setColor(mFontColor);
        mPaint.setTextSize(mFontSize);
        String startTime = TextUtils.isEmpty(mStartTime) ? "" : mStartTime;
        String endTime = TextUtils.isEmpty(mEndTime) ? "" : mEndTime;
        String sunrise = "日出時間:" + startTime;
        String sunset = "日落時間:" + endTime;
        canvas.drawText(sunrise, mWidth / 2 - mRadius, mRadius + 50 + marginTop, mPaint);
        canvas.drawText(sunset, mWidth / 2 + mRadius - getTextWidth(mPaint, sunset), mRadius + 50 + marginTop, mPaint);
    }

    /**
     * 精確計算文字寬度
     *
     * @param paint 畫筆
     * @param str   字串文字
     * @return
     */
    public static int getTextWidth(Paint paint, String str)
    {
        int iRet = 0;
        if (str != null && str.length() > 0)
        {
            int len = str.length();
            float[] widths = new float[len];
            paint.getTextWidths(str, widths);
            for (int j = 0; j < len; j++)
            {
                iRet += (int) Math.ceil(widths[j]);
            }
        }
        return iRet;
    }

    /**
     * 根據日出和日落時間計算出一天總共的時間:單位為分鐘
     *
     * @param startTime 日出時間
     * @param endTime   日落時間
     * @return
     */
    private float calculateTime(String startTime, String endTime)
    {

        if (checkTime(startTime, endTime))
        {
            String startTimes[] = startTime.split(":");
            String endTimes[] = endTime.split(":");

            float startHour = Float.parseFloat(startTimes[0]);
            float startMinute = Float.parseFloat(startTimes[1]);

            float endHour = Float.parseFloat(endTimes[0]);
            float endMinute = Float.parseFloat(endTimes[1]);

            float needTime = (endHour - startHour - 1) * 60 + (60 - startMinute) + endMinute;
            return needTime;
        }
        return 0;
    }

    /**
     * 對所給的時間做一下簡單的資料校驗
     *
     * @param startTime
     * @param endTime
     * @return
     */
    private boolean checkTime(String startTime, String endTime)
    {
        if (TextUtils.isEmpty(startTime) || TextUtils.isEmpty(endTime)
                || !startTime.contains(":") || !endTime.contains(":"))
        {
            return false;
        }

        String startTimes[] = startTime.split(":");
        String endTimes[] = endTime.split(":");
        float startHour = Float.parseFloat(startTimes[0]);
        float startMinute = Float.parseFloat(startTimes[1]);

        float endHour = Float.parseFloat(endTimes[0]);
        float endMinute = Float.parseFloat(endTimes[1]);

        //如果所給的時間(hour)小於日出時間(hour)或者大於日落時間(hour)
        if ((startHour < Float.parseFloat(mStartTime.split(":")[0]))
                || (endHour > Float.parseFloat(mEndTime.split(":")[0])))
        {
            return false;
        }

        //如果所給時間與日出時間:hour相等,minute小於日出時間
        if ((startHour == Float.parseFloat(mStartTime.split(":")[0]))
                && (startMinute < Float.parseFloat(mStartTime.split(":")[1])))
        {
            return false;
        }

        //如果所給時間與日落時間:hour相等,minute大於日落時間
        if ((startHour == Float.parseFloat(mEndTime.split(":")[0]))
                && (endMinute > Float.parseFloat(mEndTime.split(":")[1])))
        {
            return false;
        }

        if (startHour < 0 || endHour < 0
                || startHour > 23 || endHour > 23
                || startMinute < 0 || endMinute < 0
                || startMinute > 60 || endMinute > 60)
        {
            return false;
        }
        return true;
    }

    /**
     * 根據具體的時間、日出日落的時間差值 計算出所給時間的百分佔比
     *
     * @param totalTime 日出日落的總時間差
     * @param needTime  當前時間與日出時間差
     * @return
     */
    private String formatTime(float totalTime, float needTime)
    {
        if (totalTime == 0)
            return "0.00";
        DecimalFormat decimalFormat = new DecimalFormat("0.00");//保留2位小數,構造方法的字元格式這裡如果小數不足2位,會以0補足.
        return decimalFormat.format(needTime / totalTime);//format 返回的是字串
    }

    private void setAnimation(float startAngle, float currentAngle, int duration)
    {
        ValueAnimator sunAnimator = ValueAnimator.ofFloat(startAngle, currentAngle);
        sunAnimator.setDuration(duration);
        sunAnimator.setTarget(currentAngle);
        sunAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
        {
            @Override
            public void onAnimationUpdate(ValueAnimator animation)
            {
                //每次要繪製的圓弧角度
                mCurrentAngle = (float) animation.getAnimatedValue();
                invalidateView();
            }

        });
        sunAnimator.start();
    }

    private void invalidateView()
    {
        //繪製太陽的x座標和y座標
        positionX = mWidth / 2 - (float) (mRadius * Math.cos((mCurrentAngle) * Math.PI / 180)) - 20;
        positionY = mRadius - (float) (mRadius * Math.sin((mCurrentAngle) * Math.PI / 180)) - 10;

        invalidate();
    }
}

        屬性配置:

<declare-styleable name="SunAnimationView">
        <attr name="sun_circle_color" format="color"></attr>
        <attr name="sun_font_color" format="color"></attr>
        <attr name="sun_font_size" format="dimension"></attr>
        <attr name="sun_circle_radius" format="integer"></attr>
 </declare-styleable>

         佈局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:app="http://schemas.android.com/apk/res-auto"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical">

    <com.jianzou.sunanimation.SunAnimationView
        android:id="@+id/sun_view"
        android:layout_width="match_parent"
        android:layout_height="260dp"
        app:sun_circle_color="@color/text_black_two"
        app:sun_circle_radius="150"
        app:sun_font_color="@color/text_black_three"
        app:sun_font_size="12px"/>

    <Button
        android:id="@+id/btn_set_time"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="20dp"
        android:text="設定時間"/>

</LinearLayout>

       DisplayUtils:

package com.jianzou.sunanimation;

import android.content.Context;

public class DisplayUtils
{

    public static int dp2px(Context context, float dpValue)
    {
        float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }
}

        activity使用:

public class SunAnimationActivity extends AppCompatActivity
{
    Button button;
    SunAnimationView sumView;

    private String mCurrentTime;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sun);
        initView();
    }

    private void initView()
    {
        mCurrentTime = "15:40";
        sumView = (SunAnimationView) findViewById(R.id.sun_view);
        button = (Button) findViewById(R.id.btn_set_time);
        button.setOnClickListener(new View.OnClickListener()
        {
            @Override
            public void onClick(View v)
            {
                sumView.setTimes("05:10", "18:40", mCurrentTime);
                button.setText("當前時間:" + mCurrentTime);
            }
        });
    }
}
        這裡需要說下的是,三角函式計算得到的原點座標有點偏差,因為我們本來就保留小數了,所以微調了下,還有一塊程式碼看起來很蛋疼,就是對所給的時間做簡單的校驗。好了,自己寫一寫,感覺複習了很多東西,對自定義view也有了更多認識。當然,這裡或許還有很多可改進的空間,有興趣的朋友可以自己拿去改改。不過,我發現現在上傳的demo選擇下載積分不能為0了,最少為1積分。所以也很對不住需要下載的朋友,如果自己動手的話,上面的程式碼已經很全了。