Android custom View AirConditionerView hacking

简介: package com.example.arc.view; import android.content.Context; import android.graphics.Canvas; import android.
package com.example.arc.view;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.SweepGradient;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.MeasureSpec;
import android.widget.Toast;

/**
 *                 Android custom View AirConditionerView hacking
 *
 * 声明:
 *     本人在知乎看到Android非常漂亮的自定义View文章后,因为对其绘图部分运作机制好
 * 奇,于是叫程梦真帮忙一起分析其中代码运作机制,花了两个半小时才将其整体hacking完。
 *
 *                                              2016-1-1 深圳 南山平山村 曾剑锋
 *
 *
 * 一、程序来源:
 *     1. 这种ui谁能给点思路?
 *         https://m.zhihu.com/question/38598212#answer-26400956
 *     2. github:mutexliu/ZhihuAnswer
 *         https://github.com/mutexliu/ZhihuAnswer
 *
 * 二、参考文档:
 *     1. 为什么安卓android变量命名多以小写"m"开头 ?
 *         http://www.01yun.com/mobile_development_question/20130303/194172.html
 *     2. 自定义View之onMeasure()
 *         http://blog.csdn.net/pi9nc/article/details/18764863
 *     3. MeasureSpec介绍及使用详解
 *         http://www.cnblogs.com/slider/archive/2011/11/28/2266538.html
 *     4. SweepGradient扫描渲染
 *         http://blog.csdn.net/q445697127/article/details/7867506
 *     5. SweepGradient
 *         http://developer.android.com/reference/android/graphics/SweepGradient.html
 *     6. 覆写onLayout进行layout,含自定义ViewGroup例子
 *         http://blog.csdn.net/androiddevelop/article/details/8108970
 *     7. View.MeasureSpec
 *         http://developer.android.com/reference/android/view/View.MeasureSpec.html#getMode(int)
 *
 * 三、绘图的三个步骤:
 *     1. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec);
 *     2. protected void onLayout(boolean changed, int left, int top, int right, int bottom);
 *         View中不需要实现。
 *     3. protected void onDraw(Canvas canvas);
 *          
 */
public class AirConditionerView extends View {
    // 设置当前的温度
    // 这个值只能在16-28度之间,下面的private void checkTemperature(float t)方法中有限制
    private float mTemperature = 24f;
    // 画圆的paint
    private Paint mArcPaint;
    // 画上线的paint
    private Paint mLinePaint;
    // 写字的paint
    private Paint mTextPaint;

    // 这里是将控件宽度分为600份,mMinSize代表其中一份
    private float mMinSize;
    // 设置空心边框的宽度,其实就是圆弧的宽度
    private float mGapWidth;
    // 内圆半径
    private float mInnerRadius;
    // 外圆半径
    private float mRadius;
    // 中线点
    private float mCenter;

    // 圆弧矩形
    private RectF mArcRect;
    // 渐变渲染
    private SweepGradient mSweepGradient;

    // 接下来的三个构造函数是固定的写法,最后调用自己定义的的init方法
    public AirConditionerView(Context context) {
        super(context, null);
    }

    public AirConditionerView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public AirConditionerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init(){
        // Paint.Style.FILL              : 填充内部
        // Paint.Style.FILL_AND_STROKE    : 填充内部和描边
        // Paint.Style.STROKE            : 仅描边

        // 画圆弧的笔
        mArcPaint = new Paint();
        mArcPaint.setStyle(Paint.Style.STROKE);
        // 设置抗锯齿
        mArcPaint.setAntiAlias(true);

        // 画圆弧上的线的笔
        mLinePaint = new Paint();
        mLinePaint.setStyle(Paint.Style.STROKE);
        mLinePaint.setAntiAlias(true);
        // 设置笔的颜色
        mLinePaint.setColor(0xffdddddd);

        // 写字的笔
        mTextPaint = new Paint();
        mTextPaint.setAntiAlias(true);
        mTextPaint.setColor(0xff64646f);
    }

    private void initSize(){
        //寬度
        mGapWidth = 56*mMinSize;
        //内圆半径
        mInnerRadius = 180*mMinSize;
        mCenter = 300*mMinSize;
        //外圆半径
        mRadius = 208*mMinSize;

        // 包含圆弧的矩形
        mArcRect = new RectF(mCenter-mRadius, mCenter-mRadius, mCenter+mRadius, mCenter+mRadius);

        // 设置渐变色、渐变位置
        /**
         * A subclass of Shader that draws a sweep gradient around a center point.
         * 
         * Parameters
         * cx            The x-coordinate of the center
         * cy            The y-coordinate of the center
         * colors        The colors to be distributed between around the center. There must be at least 2 colors in the array.
         * positions    May be NULL. The relative position of each corresponding color in the colors array, beginning with 0 and ending with 1.0. If the values are not monotonic, the drawing may produce unexpected results. If positions is NULL, then the colors are automatically spaced evenly.
         */
        int[] colors = {
            0xFFE5BD7D,0xFFFAAA64,
            0xFFFFFFFF, 0xFF6AE2FD,
            0xFF8CD0E5, 0xFFA3CBCB,
            0xFFBDC7B3, 0xFFD1C299, 
            0xFFE5BD7D, 
        };
        float[] positions = {0,1f/8,2f/8,3f/8,4f/8,5f/8,6f/8,7f/8,1};
        mSweepGradient = new SweepGradient(mCenter, mCenter, colors , positions);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        /*****************   变色弧形部分  ************************/
        // draw arc 設置空心邊框的寬度
        // Set the width for stroking. Pass 0 to stroke in hairline mode. Hairlines always draws a single pixel independent of the canva's matrix.
        mArcPaint.setStrokeWidth(mGapWidth);
        int gapDegree = getDegree();
        // 绘制梯度渐变  
        // Set or clear the shader object.
        mArcPaint.setShader(mSweepGradient);
        //画渐变色弧形
        // Draw the specified arc, which will be scaled to fit inside the specified oval.
        // If the start angle is negative or >= 360, the start angle is treated as start angle modulo 360.
        // If the sweep angle is >= 360, then the oval is drawn completely. Note that this differs slightly from SkPath::arcTo, which treats the sweep angle modulo 360. If the sweep angle is negative, the sweep angle is treated as sweep angle modulo 360
        // The arc is drawn clockwise. An angle of 0 degrees correspond to the geometric angle of 0 degrees (3 o'clock on a watch.)
        // Parameters
        //    oval            The bounds of oval used to define the shape and size of the arc
        //    startAngle    Starting angle (in degrees) where the arc begins
        //    sweepAngle    Sweep angle (in degrees) measured clockwise
        //    useCenter        If true, include the center of the oval in the arc, and close it if it is being stroked. This will draw a wedge
        //    paint            The paint used to draw the arc
        canvas.drawArc(mArcRect, -225, gapDegree + 225, false, mArcPaint);
        
        
        /*****************   白色弧形部分  ************************/
        mArcPaint.setShader(null);
        mArcPaint.setColor(Color.WHITE);
        
        //画渐变色弧形
        canvas.drawArc(mArcRect, gapDegree, 45 - gapDegree, false, mArcPaint);

        // draw line
        /*****************   画线部分  ************************/
        mLinePaint.setStrokeWidth(mMinSize*1.5f);
        // 将圆等分成120份,每份占360度的3度
        for(int i = 0; i<120; i++){
            // (75-45)*3 = 30*3 = 90,不绘直线部分占90度,正好符合空白区
            if(i<=45 || i >= 75){ 
                float top = mCenter-mInnerRadius-mGapWidth;
                // 2度分成15格
                if(i%15 == 0){
                    top = top - 20*mMinSize;
                }
                // 绘制垂直线
                canvas.drawLine(mCenter, mCenter - mInnerRadius, mCenter, top, mLinePaint);
            }
            /**
             * Preconcat the current matrix with the specified rotation.
             * 
             * Parameters
             * degrees    The amount to rotate, in degrees
             * px    The x-coord for the pivot point (unchanged by the rotation)
             * py    The y-coord for the pivot point (unchanged by the rotation)
             *
             * 坐标系旋转3度,不是已经绘制的图形旋转3度
             */
            canvas.rotate(3,mCenter,mCenter);
        }

        // draw text
        /*****************   弧形外部显示温度度数文字 部分  ************************/
        mTextPaint.setTextSize(mMinSize*30);
        mTextPaint.setTextAlign(Paint.Align.CENTER);
        for(int i = 16; i<29; i+=2){
            /**
             * 计算文字在圆弧边缘的位置,用到三角函数计算。
             */
            float r = mInnerRadius+mGapWidth + 40*mMinSize;
            float x = (float) (mCenter + r*Math.cos((26-i)/2*Math.PI/4));
            float y = (float) (mCenter - r*Math.sin((26-i)/2*Math.PI/4));
            canvas.drawText(""+i, x, y - ((mTextPaint.descent() + mTextPaint.ascent()) / 2), mTextPaint);
        }
    }

    private int getDegree(){
        checkTemperature(mTemperature);
        return -225 + (int)((mTemperature-16)/12*90+0.5f)*3;
    }
    /*****************   用来控制事件的三个方法  ************************/
    private void checkTemperature(float t){
        if(t<16 || t > 28){
            throw new RuntimeException("Temperature out of range");
        }
    }

    public void setTemperature(float t){
        checkTemperature(t);
        mTemperature = t;
        // To force a view to draw, call invalidate().
        invalidate();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // 获取触摸位置
        float x = event.getX();
        float y = event.getY();

        // 计算触摸点到中心点的距离,判断该点是否在圆弧之内
        double distance = Math.sqrt((x - mCenter) * (x - mCenter) + (y - mCenter) * (y - mCenter));
        if(distance < mInnerRadius || distance > mInnerRadius + mGapWidth){
            return false;
        }
        // 判断是否在空白区
        double degree = Math.atan2(-(y-mCenter),x-mCenter);
        if(-3*Math.PI/4<degree && degree < -Math.PI/4){
            return false;
        }
        // 计算角度,并转换为对应的温度,设置当前温度,设置温度之后会自动对View进行重绘
        if(degree < -3*Math.PI/4){
            degree = degree + 2*Math.PI;
        }
        float t = (float) (26 - degree*8/Math.PI);
        setTemperature(t);

        return true;
    }
    /*****************   为了获得mMinSize  ************************/
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int desiredWidth = Integer.MAX_VALUE;

        /**
         * Extracts the mode from the supplied measure specification.
         * 
         * Parameters
         *     measureSpec    the measure specification to extract the mode from
         * Returns
         *     UNSPECIFIED, AT_MOST or EXACTLY
         */
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        int width;
        int height;

        //Measure Width
        if (widthMode == MeasureSpec.EXACTLY) {
            //Must be this size
            width = widthSize;
            //Toast.makeText(getContext(), "MeasureSpec.EXACTLY", 0).show();
        } else if (widthMode == MeasureSpec.AT_MOST) {
            width = Math.min(desiredWidth, widthSize);
            //Toast.makeText(getContext(), "MeasureSpec.AT_MOST"+desiredWidth+" "+widthSize, 0).show();
        } else {
            //Be whatever you want
            width = desiredWidth;
            //Toast.makeText(getContext(), "MeasureSpec.UNSPECIFIED", 0).show();
        }
        mMinSize = width/600f;
        
        int size = width;
        initSize();
        //Measure Height
        if (heightMode == MeasureSpec.EXACTLY) {
            //Must be this size
            height = heightSize;
        } else if (heightMode == View.MeasureSpec.AT_MOST) {
            //Can't be bigger than...
            height = Math.min(size, heightSize);
        } else {
            //Be whatever you want
            height = size;
        }

        //MUST CALL THIS
        /** 
         * This method must be called by onMeasure(int, int) to store the measured width and measured height. Failing to do so will trigger an exception at measurement time.
         * 
         * Parameters
         *     measuredWidth    The measured width of this view. May be a complex bit mask as defined by MEASURED_SIZE_MASK and MEASURED_STATE_TOO_SMALL.
         *     measuredHeight    The measured height of this view. May be a complex bit mask as defined by MEASURED_SIZE_MASK and MEASURED_STATE_TOO_SMALL.
         */
        setMeasuredDimension(width, height);
    }

}

 

目录
相关文章
|
7月前
|
Android开发 UED 计算机视觉
Android自定义view之线条等待动画(灵感来源:金铲铲之战)
本文介绍了一款受游戏“金铲铲之战”启发的Android自定义View——线条等待动画的实现过程。通过将布局分为10份,利用`onSizeChanged`测量最小长度,并借助画笔绘制动态线条,实现渐变伸缩效果。动画逻辑通过四个变量控制线条的增长与回退,最终形成流畅的等待动画。代码中详细展示了画笔初始化、线条绘制及动画更新的核心步骤,并提供完整源码供参考。此动画适用于加载场景,提升用户体验。
529 5
Android自定义view之线条等待动画(灵感来源:金铲铲之战)
|
7月前
|
Android开发 开发者
Android设置View是否可用
在Android开发中,有时需要将布局设置为不可点击状态(失去焦点)。常见的解决方法是使用`setOnClickListener(null)`,但本文介绍一种更通用的方式:通过封装`setViewEnabled`方法实现。该方法可递归设置View及其子View的启用状态,支持传入目标View和布尔值(`true`为可用,`false`为禁用)。例如,调用`setViewEnabled(edittext, false)`即可禁用EditText。文章附有源码及示例动图,帮助开发者快速理解与应用。
166 1
|
7月前
|
Android开发
Android自定义view之利用PathEffect实现动态效果
本文介绍如何在Android自定义View中利用`PathEffect`实现动态效果。通过改变偏移量,结合`PathEffect`的子类(如`CornerPathEffect`、`DashPathEffect`、`PathDashPathEffect`等)实现路径绘制的动态变化。文章详细解析了各子类的功能与参数,并通过案例代码展示了如何使用`ComposePathEffect`组合效果,以及通过修改偏移量实现动画。最终效果为一个菱形图案沿路径运动,源码附于文末供参考。
129 0
|
7月前
|
Android开发 开发者
Android自定义view之利用drawArc方法实现动态效果
本文介绍了如何通过Android自定义View实现动态效果,重点使用`drawArc`方法完成圆弧动画。首先通过`onSizeChanged`进行测量,初始化画笔属性,设置圆弧相关参数。核心思路是不断改变圆弧扫过角度`sweepAngle`,并调用`invalidate()`刷新View以实现动态旋转效果。最后附上完整代码与效果图,帮助开发者快速理解并实践这一动画实现方式。
184 0
|
7月前
|
Android开发 数据安全/隐私保护 开发者
Android自定义view之模仿登录界面文本输入框(华为云APP)
本文介绍了一款自定义输入框的实现,包含静态效果、hint值浮动动画及功能扩展。通过组合多个控件完成界面布局,使用TranslateAnimation与AlphaAnimation实现hint文字上下浮动效果,支持密码加密解密显示、去除键盘回车空格输入、光标定位等功能。代码基于Android平台,提供完整源码与attrs配置,方便复用与定制。希望对开发者有所帮助。
130 0
|
7月前
|
XML Java Android开发
Android自定义view之网易云推荐歌单界面
本文详细介绍了如何通过自定义View实现网易云音乐推荐歌单界面的效果。首先,作者自定义了一个圆角图片控件`MellowImageView`,用于绘制圆角矩形图片。接着,通过将布局放入`HorizontalScrollView`中,实现了左右滑动功能,并使用`ViewFlipper`添加图片切换动画效果。文章提供了完整的代码示例,包括XML布局、动画文件和Java代码,最终展示了实现效果。此教程适合想了解自定义View和动画效果的开发者。
334 65
Android自定义view之网易云推荐歌单界面
|
7月前
|
XML 前端开发 Android开发
一篇文章带你走近Android自定义view
这是一篇关于Android自定义View的全面教程,涵盖从基础到进阶的知识点。文章首先讲解了自定义View的必要性及简单实现(如通过三个构造函数解决焦点问题),接着深入探讨Canvas绘图、自定义属性设置、动画实现等内容。还提供了具体案例,如跑马灯、折线图、太极图等。此外,文章详细解析了View绘制流程(measure、layout、draw)和事件分发机制。最后延伸至SurfaceView、GLSurfaceView、SVG动画等高级主题,并附带GitHub案例供实践。适合希望深入理解Android自定义View的开发者学习参考。
675 84
|
7月前
|
Android开发 开发者
Android自定义View之不得不知道的文件attrs.xml(自定义属性)
本文详细介绍了如何通过自定义 `attrs.xml` 文件实现 Android 自定义 View 的属性配置。以一个包含 TextView 和 ImageView 的 DemoView 为例,讲解了如何使用自定义属性动态改变文字内容和控制图片显示隐藏。同时,通过设置布尔值和点击事件,实现了图片状态的切换功能。代码中展示了如何在构造函数中解析自定义属性,并通过方法 `setSetting0n` 和 `setbackeguang` 实现功能逻辑的优化与封装。此示例帮助开发者更好地理解自定义 View 的开发流程与 attrs.xml 的实际应用。
182 2
Android自定义View之不得不知道的文件attrs.xml(自定义属性)
|
7月前
|
前端开发 Android开发 UED
讲讲Android为自定义view提供的SurfaceView
本文详细介绍了Android中自定义View时使用SurfaceView的必要性和实现方式。首先分析了在复杂绘制逻辑和高频界面更新场景下,传统View可能引发卡顿的问题,进而引出SurfaceView作为解决方案。文章通过Android官方Demo展示了SurfaceView的基本用法,包括实现`SurfaceHolder.Callback2`接口、与Activity生命周期绑定、子线程中使用`lockCanvas()`和`unlockCanvasAndPost()`方法完成绘图操作。
200 3
|
7月前
|
Android开发 开发者
Android自定义view之围棋动画(化繁为简)
本文介绍了Android自定义View的动画实现,通过两个案例拓展动态效果。第一个案例基于`drawArc`方法实现单次动画,借助布尔值控制动画流程。第二个案例以围棋动画为例,从简单的小球直线运动到双向变速运动,最终实现循环动画效果。代码结构清晰,逻辑简明,展示了如何化繁为简实现复杂动画,帮助读者拓展动态效果设计思路。文末提供完整源码,适合初学者和进阶开发者学习参考。
135 0
Android自定义view之围棋动画(化繁为简)

热门文章

最新文章