自定义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积分。所以也很对不住需要下载的朋友,如果自己动手的话,上面的代码已经很全了。
       

  • 26
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 24
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 24
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值