50、自定义View练习(四)高仿小米时钟-使用Camera和Matrix实现3D效果

*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

本文出自:猴菇先生的博客

http://blog.csdn.net/qq_31715429/article/details/54668668

继续练习自定义View。。毕竟熟才能生巧。一直觉得小米的时钟很精美,那这次就搞它~这次除了练习自定义View,还涉及到使用Camera和Matrix实现3D效果。

附上github地址:

https://github.com/MonkeyMushroom/MiClockView

欢迎start~

一个这样的效果,在绘制的时候最好选择一个方向一步一步的绘制,这里我选择由外到内、由深到浅的方向来绘制,代码步骤如下:

1、首先老一套~新建attrs.xml文件,编写自定义属性如时钟背景色、亮色(用于分针、秒针、渐变终止色)、暗色(圆弧、刻度线、时针、渐变起始色),新建MiClockView继承View,重写构造方法,获取自定义属性值,初始化Paint、Path以及画圆、弧需要的RectF等东东,重写onMeasure计算宽高,这里不再啰嗦~刚开始学自定义View的同学建议从我的前几篇博客看起

2、由于onSizeChanged方法在构造方法、onMeasure之后,又在onDraw之前,此时已经完成全局变量初始化,也得到了控件的宽高,所以可以在这个方法中确定一些与宽高有关的数值,比如这个View的半径啊、padding值等,方便绘制的时候计算大小和位置:

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    //宽和高分别去掉padding值,取min的一半即表盘的半径
    mRadius = Math.min(w - getPaddingLeft() - getPaddingRight(),
            h - getPaddingTop() - getPaddingBottom()) / 2;
    //加一个默认的padding值,为了防止用camera旋转时钟时造成四周超出view大小
    mDefaultPadding = 0.12f * mRadius;//根据比例确定默认padding大小
    //为了适配控件大小match_parent、wrap_content、精确数值以及padding属性
    mPaddingLeft = mDefaultPadding + w / 2 - mRadius + getPaddingLeft();
    mPaddingTop = mDefaultPadding + h / 2 - mRadius + getPaddingTop();
    mPaddingRight = mPaddingLeft;
    mPaddingBottom = mPaddingTop;
    mScaleLength = 0.12f * mRadius;//根据比例确定刻度线长度
    mScaleArcPaint.setStrokeWidth(mScaleLength);//刻度盘的弧宽
    mScaleLinePaint.setStrokeWidth(0.012f * mRadius);//刻度线的宽度
    //梯度扫描渐变,以(w/2,h/2)为中心点,两种起止颜色梯度渐变
    //float数组表示,[0,0.75)为起始颜色所占比例,[0.75,1}为起止颜色渐变所占比例
    mSweepGradient = new SweepGradient(w / 2, h / 2,
            new int[]{mDarkColor, mLightColor}, new float[]{0.75f, 1});
}

3、准备工作做的差不多了,那就开始绘制,根据方向我先确定最外层的小时时间文本的位置及其旁边的四个弧:

注意两位数字的宽度和一位数的宽度是不一样的,在计算的时候一定要注意

    String timeText = "12";
    mTextPaint.getTextBounds(timeText, 0, timeText.length(), mTextRect);
    int textLargeWidth = mTextRect.width();//两位数字的宽
    mCanvas.drawText("12", getWidth() / 2 - textLargeWidth / 2, mPaddingTop + mTextRect.height(), mTextPaint);
    timeText = "3";
    mTextPaint.getTextBounds(timeText, 0, timeText.length(), mTextRect);
    int textSmallWidth = mTextRect.width();//一位数字的宽
    mCanvas.drawText("3", getWidth() - mPaddingRight - mTextRect.height() / 2 - textSmallWidth / 2,
            getHeight() / 2 + mTextRect.height() / 2, mTextPaint);
    mCanvas.drawText("6", getWidth() / 2 - textSmallWidth / 2, getHeight() - mPaddingBottom, mTextPaint);
    mCanvas.drawText("9", mPaddingLeft + mTextRect.height() / 2 - textSmallWidth / 2,
            getHeight() / 2 + mTextRect.height() / 2, mTextPaint);

我计算文本的宽高一般采用的方法是,new一个Rect,然后再绘制时调用

mTextPaint.getTextBounds(timeText, 0, timeText.length(), mTextRect);

将这个文本的范围赋值给这个mTextRect,此时mTextRect.width()就是这段文本的宽,mTextRect.height()就是这段文本的高。

画文本旁边的四个弧:

mCircleRectF.set(mPaddingLeft + mTextRect.height() / 2 + mCircleStrokeWidth / 2,
        mPaddingTop + mTextRect.height() / 2 + mCircleStrokeWidth / 2,
        getWidth() - mPaddingRight - mTextRect.height() / 2 + mCircleStrokeWidth / 2,
        getHeight() - mPaddingBottom - mTextRect.height() / 2 + mCircleStrokeWidth / 2);
for (int i = 0; i < 4; i++) {
    mCanvas.drawArc(mCircleRectF, 5 + 90 * i, 80, false, mCirclePaint);
}

计算圆弧外接矩形的范围别忘了加上圆弧线宽的一半

4、再往里是刻度盘,画这个刻度盘的思路是现在底层画一个mScaleLength宽度的圆,并设置SweepGradient渐变,上面再画一圈背景色的刻度线。获得SweepGradient的Matrix对象,通过不断旋转mGradientMatrix的角度实现刻度盘的旋转效果:

/**
 * 画一圈梯度渲染的亮暗色渐变圆弧,重绘时不断旋转,上面盖一圈背景色的刻度线
 */
private void drawScaleLine() {
    mScaleArcRectF.set(mPaddingLeft + 1.5f * mScaleLength + mTextRect.height() / 2,
            mPaddingTop + 1.5f * mScaleLength + mTextRect.height() / 2,
            getWidth() - mPaddingRight - mTextRect.height() / 2 - 1.5f * mScaleLength,
            getHeight() - mPaddingBottom - mTextRect.height() / 2 - 1.5f * mScaleLength);

    //matrix默认会在三点钟方向开始颜色的渐变,为了吻合
    //钟表十二点钟顺时针旋转的方向,把秒针旋转的角度减去90度
    mGradientMatrix.setRotate(mSecondDegree - 90, getWidth() / 2, getHeight() / 2);
    mSweepGradient.setLocalMatrix(mGradientMatrix);
    mScaleArcPaint.setShader(mSweepGradient);
    mCanvas.drawArc(mScaleArcRectF, 0, 360, false, mScaleArcPaint);
    //画背景色刻度线
    mCanvas.save();
    for (int i = 0; i < 200; i++) {
        mCanvas.drawLine(getWidth() / 2, mPaddingTop + mScaleLength + mTextRect.height() / 2,
                getWidth() / 2, mPaddingTop + 2 * mScaleLength + mTextRect.height() / 2, mScaleLinePaint);
        mCanvas.rotate(1.8f, getWidth() / 2, getHeight() / 2);
    }
    mCanvas.restore();
}

这里有一个全局变量mSecondDegree,即秒针旋转的角度,需要根据当前时间动态获取:

/**
 * 获取当前 时分秒 所对应的角度
 * 为了不让秒针走得像老式挂钟一样僵硬,需要精确到毫秒
 */
private void getTimeDegree() {
    Calendar calendar = Calendar.getInstance();
    float milliSecond = calendar.get(Calendar.MILLISECOND);
    float second = calendar.get(Calendar.SECOND) + milliSecond / 1000;
    float minute = calendar.get(Calendar.MINUTE) + second / 60;
    float hour = calendar.get(Calendar.HOUR) + minute / 60;
    mSecondDegree = second / 60 * 360;
    mMinuteDegree = minute / 60 * 360;
    mHourDegree = hour / 12 * 360;
}

5、然后就是画秒针,用Path绘制一个指向12点钟的三角形,通过不断旋转画布实现秒针的旋转:

/**
 * 画秒针,根据不断变化的秒针角度旋转画布
 */
private void drawSecondHand() {
    mCanvas.save();
    mCanvas.rotate(mSecondDegree, getWidth() / 2, getHeight() / 2);
    mSecondHandPath.reset();
    float offset = mPaddingTop + mTextRect.height() / 2;
    mSecondHandPath.moveTo(getWidth() / 2, offset + 0.27f * mRadius);
    mSecondHandPath.lineTo(getWidth() / 2 - 0.05f * mRadius, offset + 0.35f * mRadius);
    mSecondHandPath.lineTo(getWidth() / 2 + 0.05f * mRadius, offset + 0.35f * mRadius);
    mSecondHandPath.close();
    mSecondHandPaint.setColor(mLightColor);
    mCanvas.drawPath(mSecondHandPath, mSecondHandPaint);
    mCanvas.restore();
}

6、看实现图,时针在分针之上并且比分针颜色浅,那我就先画时针,仍然是Path,并且针头为圆弧状,那么就用二阶贝赛尔曲线,路径为moveTo( A),lineTo(B),quadTo(C,D),lineTo(E),close.

/**
 * 画时针,根据不断变化的时针角度旋转画布
 * 针头为圆弧状,使用二阶贝塞尔曲线
 */
private void drawHourHand() {
    mCanvas.save();
    mCanvas.rotate(mHourDegree, getWidth() / 2, getHeight() / 2);
    mHourHandPath.reset();
    float offset = mPaddingTop + mTextRect.height() / 2;
    mHourHandPath.moveTo(getWidth() / 2 - 0.02f * mRadius, getHeight() / 2);
    mHourHandPath.lineTo(getWidth() / 2 - 0.01f * mRadius, offset + 0.5f * mRadius);
    mHourHandPath.quadTo(getWidth() / 2, offset + 0.48f * mRadius,
            getWidth() / 2 + 0.01f * mRadius, offset + 0.5f * mRadius);
    mHourHandPath.lineTo(getWidth() / 2 + 0.02f * mRadius, getHeight() / 2);
    mHourHandPath.close();
    mCanvas.drawPath(mHourHandPath, mHourHandPaint);
    mCanvas.restore();
}

7、然后是分针,按照时针的思路:

/**
 * 画分针,根据不断变化的分针角度旋转画布
 */
private void drawMinuteHand() {
    mCanvas.save();
    mCanvas.rotate(mMinuteDegree, getWidth() / 2, getHeight() / 2);
    mMinuteHandPath.reset();
    float offset = mPaddingTop + mTextRect.height() / 2;
    mMinuteHandPath.moveTo(getWidth() / 2 - 0.01f * mRadius, getHeight() / 2);
    mMinuteHandPath.lineTo(getWidth() / 2 - 0.008f * mRadius, offset + 0.38f * mRadius);
    mMinuteHandPath.quadTo(getWidth() / 2, offset + 0.36f * mRadius,
            getWidth() / 2 + 0.008f * mRadius, offset + 0.38f * mRadius);
    mMinuteHandPath.lineTo(getWidth() / 2 + 0.01f * mRadius, getHeight() / 2);
    mMinuteHandPath.close();
    mCanvas.drawPath(mMinuteHandPath, mMinuteHandPaint);
    mCanvas.restore();
}

8、最后由于path是close的,所以干脆画两个圆盖在上面:

/**
 * 画指针的连接圆圈,盖住指针path在圆心的连接线
 */
private void drawCoverCircle() {
    mCanvas.drawCircle(getWidth() / 2, getHeight() / 2, 0.05f * mRadius, mSecondHandPaint);
    mSecondHandPaint.setColor(mBackgroundColor);
    mCanvas.drawCircle(getWidth() / 2, getHeight() / 2, 0.025f * mRadius, mSecondHandPaint);
}

9、终于画完了,onDraw部分就是这样

@Override
protected void onDraw(Canvas canvas) {
    mCanvas = canvas;
    getTimeDegree();
    drawTimeText();
    drawScaleLine();
    drawSecondHand();
    drawHourHand();
    drawMinuteHand();
    drawCoverCircle();
    invalidate();
}

绘制的时候,尤其是像这样圆形view,灵活运用

canvas.save();
canvas.rotate(mDegree, mCenterX, mCenterY);
<!-- draw something -->
canvas.restore();

这一套组合拳可以减少不少三角函数、角度弧度相关的计算。

10、辣么接下来就是如何实现触摸使钟表3D旋转

借助Camera类和Matrix类,在构造方法中:

Matrix mCameraMatrix = new Matrix();
Camera mCamera = new Camera();
/**
 * 设置3D时钟效果,触摸矩阵的相关设置、照相机的旋转大小
 * 应用在绘制图形之前,否则无效
 *
 * @param rotateX 绕X轴旋转的大小
 * @param rotateY 绕Y轴旋转的大小
 */
private void setCameraRotate(float rotateX, float rotateY) {
    mCameraMatrix.reset();
    mCamera.save();
    mCamera.rotateX(mCameraRotateX);//绕x轴旋转角度
    mCamera.rotateY(mCameraRotateY);//绕y轴旋转角度
    mCamera.getMatrix(mCameraMatrix);//相关属性设置到matrix中
    mCamera.restore();
    //camera在view左上角那个点,故旋转默认是以左上角为中心旋转
    //故在动作之前pre将matrix向左移动getWidth()/2长度,向上移动getHeight()/2长度
    mCameraMatrix.preTranslate(-getWidth() / 2, -getHeight() / 2);
    //在动作之后post再回到原位
    mCameraMatrix.postTranslate(getWidth() / 2, getHeight() / 2);
    mCanvas.concat(mCameraMatrix);//matrix与canvas相关联
}

这段代码除了camera的旋转、平移、缩放之类的操作之外,剩下的代码一般是固定的

全局变量mCameraRotateX和mCameraRotateY应该与此时手指触摸坐标相关联动态获取:

@Override
public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            getCameraRotate(event);
            break;
        case MotionEvent.ACTION_MOVE:
            //根据手指坐标计算camera应该旋转的大小
            getCameraRotate(event);
            break;
    }
    return true;
}

Camera的坐标系和View的坐标系是不一样的

View坐标系是二维的,原点在屏幕左上角,右为x轴正方向,下为y轴正方向;而Camera坐标系是三维的,原点在屏幕左上角,右为x轴正方向,上为y轴正方向,屏幕向里为z轴正方向

/**
 * 获取camera旋转的大小
 * 注意view坐标与camera坐标方向的转换
 */
private void getCameraRotate(MotionEvent event) {
    float rotateX = -(event.getY() - getHeight() / 2);
    float rotateY = (event.getX() - getWidth() / 2);
    //求出此时旋转的大小与半径之比
    float percentX = rotateX / mRadius;
    float percentY = rotateY / mRadius;
    if (percentX > 1) {
        percentX = 1;
    } else if (percentX < -1) {
        percentX = -1;
    }
    if (percentY > 1) {
        percentY = 1;
    } else if (percentY < -1) {
        percentY = -1;
    }
    //最终旋转的大小按比例匀称改变
    mCameraRotateX = percentX * mMaxCameraRotate;
    mCameraRotateY = percentY * mMaxCameraRotate;
}

11、最后在onTouchEvent中松开手指时加一个复原并晃动的动画

case MotionEvent.ACTION_UP:
    //松开手指,时钟复原并伴随晃动动画
    ValueAnimator animX = getShakeAnim(mCameraRotateX, 0);
    animX.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator valueAnimator) {
            mCameraRotateX = (float) valueAnimator.getAnimatedValue();
        }
    });
    ValueAnimator animY = getShakeAnim(mCameraRotateY, 0);
    animY.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator valueAnimator) {
            mCameraRotateY = (float) valueAnimator.getAnimatedValue();
        }
    });
    break;
/**
 * 使用OvershootInterpolator完成时钟晃动动画
 */
private ValueAnimator getShakeAnim(float start, float end) {
    ValueAnimator anim = ValueAnimator.ofFloat(start, end);
    anim.setInterpolator(new OvershootInterpolator(10));
    anim.setDuration(500);
    anim.start();
    return anim;
}

终于写完了,这个MiClockView适配也做的差不多了,时间也是同步的手机时间,一般可以拿来就用了~

时间: 2024-10-27 03:28:03

50、自定义View练习(四)高仿小米时钟-使用Camera和Matrix实现3D效果的相关文章

android自定义View之(六)------高仿华为荣耀3C的圆形刻度比例图(ShowPercentView)

为什么写这篇文章: 显示当前的容量所占的比例,表现当前计划的进度,一般都会采用百分比的方式,而图形显示,以其一目了然的直观性和赏心悦目的美观形成为了我们的当然的首选. 在图形表示百分比的方法中,我们有用画圆的圆形进度条的方法<<android自定义View之(二)------简单进度条显示样例篇>>,也有用画弧形的进度条的方法<<android自定义View之(三)------视频音量调控样例>>,今天看到华为荣耀3C的一个界面: 个人觉得这个表示比例的圆形

Androidstudio如何制作一个高仿小米计算器小demo

Androidstudio如何制作一个高仿小米计算器小demo ————安德风 一.最终成品效果图: 二.界面设计布局源代码:文件名activity_main.xml (存放在jsj(我的模块名为jsj)/res/layout/activity_main.xml) 1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android="http://schemas.a

【Android自定义View实战】之仿百度加载动画,一种优雅的Loading方式

转载请注明出处:http://blog.csdn.net/linglongxin24/article/details/53470872 本文出自[DylanAndroid的博客] Android自定义View实战之仿百度加载动画一种优雅的Loading方式 第一个仿百度加载动画用ObjectAnimator属性动画操作ImageView的属性方法实现 第二个仿百度加载动画第二种实现方式用ValueAnimator原生的ondraw方法实现 第三个扔球动画-水平旋转动画 第四个扔球动画-垂直旋转动

【Android自定义View实战】之仿QQ运动步数圆弧及动画,Dylan计步中的控件StepArcView

转载请注明出处:http://blog.csdn.net/linglongxin24/article/details/52936609[DylanAndroid的csdn博客] 在之前的Android超精准计步器开发-Dylan计步中的首页用到了一个自定义控件,和QQ运动的界面有点类似,还有动画效果,下面就来讲一下这个View是如何绘制的. 1.先看效果图 2.效果图分析 功能说明:黄色的代表用户设置的总计划锻炼步数,红色的代表用户当前所走的步数. 初步分析:完全自定义View重写onDraw(

android自定义View之(四)------一键清除动画

1.前言: 自己也是参考别人的一些自定义view例子,学习了一些基本的自定义view的方法.今天,我参考了一些资料,再结合自已的一些理解,做了一个一键清除的动画.当年,我实现这个是用了几张图片,采用Frame anination的方式来实现,但是这个方法,不灵活,并且占资源,下面,我就采用自定义view的方法来实现这个功能. 2.效果图: 3.具体详细代码 3.1 \res\values\attrs_on_key_clear_circle_view.xml <resources> <de

[如何让高仿小米3手机露出狐狸尾巴]

找来找去,觉得新出的小米3还可以,就在taobao上买了一台,到货后粗略看了下跟真的一样,就没太在意是否为假货,也就没有进入软件去确认一下,就这么一个偷懒,成了悲剧:买到高仿机了.发现过程:相机无法拍照,提示存储空间不够.进入设置中查看存储,发现内部存储空间总容量约为12G,实际数据不到3G,而其他则占将近了9G之多!朋友,如果你的小米手机出现类似的情况,那么恭喜你,基本上可以判定也是高仿的……删除一个250M的视频文件后,除了实际数据显示相应减少了之外,其他部分大约也减少了700M,说明这里面

自定义View(四)——加强版的EditText

1.如何加强? 输入内容后,有面会显示一个图片,用户点击后 可以清空文本框. 2.案例构造步骤 1)在drawable文件中建一个bg_frame_search.xml文件. <?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" > <solid android:

自定义View系列--Path绘制仿支付宝支付成功动画

前言 使用支付宝付款时,我们可以看到成功或者失败都会有个动画提示,如果我们需要做这样的效果的话,当然,你可以让设计师给你做个GIF,但是我们知道图像比较耗内存的,我们自己可以用代码实现还是代码实现好点吧. 效果 实现方法 首先我们需要了解PathMeasure这个类,这个类我们可以理解为用来管理Path.我们主要看几个方法. PathMeasure(): 构造方法 ,实例化一个对象 PathMeasure(Path path,boolean isClosed):传入Path对象和是否闭合,pat

android高仿微信UI点击头像显示大图片效果

转自:http://www.cnblogs.com/Jaylong/archive/2012/09/27/androidUI.html 用过微信的朋友朋友都见过微信中点击对方头像显示会加载大图,先贴两张图片说明下: 这种UI效果对用户的体验不错,今天突然有了灵感,试着去实现,结果就出来了.. 下面说说我的思路: 1.点击图片时跳转到另一个activity,然后显示加载的效果,即progressbar 2.显示图片的之前先弹出自定义dialog,然后模拟加载一段时间后,显示整张大图片,要全屏显示,