Android自定义View---正式篇

本文转自 http://blog.csdn.net/jiangwei0910410003/article/details/42640665 稍有修改,感谢分享!

考虑到篇幅和内容相关性,将其分为两篇文章,这里主要记录自定义View的具体使用
接上文 Android自定义View—前奏篇(Paint和Canvas的使用)

自定义View的流程为:

  • 创建一个继承自View的自定义类
    当然也可以继承自任何View的子类,比如TextView,需要重写构造方法,构造方法有多个,但是必须要有两个参数的构造方法:
public LabelView(Context context, AttributeSet attrs)
  • 定义属性
    也就是自定义View的各种属性,可以在xml文件中使用的属性,比如color,textSize等

  • 重写onMeasure方法(非必须)
    用于计算View在屏幕中的大小

  • 重写onDraw方法
    用于在屏幕上绘制自定义View这里主要用到Paint和Canvas

  • 重写onTouch方法
    用于获取对屏幕的触摸事件,创建能够与用户交互的自定义View

1. 自定义属性的使用

在开始自定义View之前,先总结下自定义属性,很多时候自定义View都需要用到自定义属性。

1.1 创建attrs.xml文件,编辑自定义属性的内容

在values目录下创建自定义属性的attrs.xml文件(一般为这个文件名,当然也可以是其他命名):
这里写图片描述

这样就定义好了一个自定义属性集合“CircleView”,这个集合的名字随便起,在自定义View中通过这个集合的名字引用到这个集合,从而获取其中的属性

1.2 在自定义的View中使用自定义属性

假设创建一个自定义View叫 CircleView.java在其构造方法中使用自定义属性的代码如下:

package com.tc.customview.view;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.util.AttributeSet;
import android.view.View;

import com.tc.customview.R;

/**
 * Created by tancen on 9/24/2015. 自定义圆形View
 */
public class CircleView extends View {

    private int mColor;
    private float mRadius;

    public CircleView(Context context) {
        super(context);
    }

    public CircleView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);//调用含三个参数的构造方法
    }

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        //获取资源文件attrs.xml中的CircleView集合
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);

        mColor = a.getColor(R.styleable.CircleView_circle_color, Color.BLUE);//获取颜色,如果没有设置这个属性,默认为蓝色
        mRadius = a.getDimension(R.styleable.CircleView_circle_radius, 10);//获取半径,如果没有设置这个属性,默认为10

        a.recycle();//释放资源
    }
}

1.3在布局文件中使用自定义属性

<RelativeLayout 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:gravity="center">

    <com.tc.customview.view.CircleView
        android:layout_width="200dp"
        android:layout_height="200dp"
        app:circle_color="@color/light_red"/>

</RelativeLayout>

关于申明命名空间,只需要保证在根布局申明的命名空间前缀“app”和自定义View中使用属性时的前缀一致就行了,当然是可以随便起名的,开心就好。

到此,自定义属性的定义,设置和调用就完成了。

2.重写onMeasure方法

一般来说,自定义控件都会去重写View的onMeasure方法,因为该方法指定该控件在屏幕上的大小

 protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) 

onMeasure传入的两个参数是由上一层控件传入的大小,有多种情况,重写该方法时需要计算控件的实际大小,然后调用setMeasuredDimension(int, int)设置实际大小。

onMeasure传入的widthMeasureSpecheightMeasureSpec不是一般的尺寸数值,而是将模式和尺寸组合在一起的数值。
获取模式:

int mode = MeasureSpec.getMode(widthMeasureSpec)

获取尺寸:

int size = MeasureSpec.getSize(widthMeasureSpec)

模式(mode)共有三种情况:

  • MeasureSpec.EXACTLY:精确尺寸,将控件的layout_width或layout_height指定为具体数值时如andorid:layout_width=”50dip”,
    或者为match_parent时,都是控件大小已经确定的情况,都是精确尺寸。

  • MeasureSpec.AT_MOST:最大尺寸,当控件的layout_width或layout_height指定为warp_content时,
    控件大小一般随着控件的子空间或内容进行变化,此时控件尺寸只要不超过父控件允许的最大尺寸即可。因此,此时的mode是AT_MOST,size给出了父控件允许的最大尺寸。

  • MeasureSpec.UNSPECIFIED:未指定尺寸,这种情况不多,一般都是父控件是AdapterView,通过measure方法传入的模式。

在重写onMeasure方法时要根据模式不同进行尺寸计算。思路很好理解,就是获取宽高的模式和尺寸,然后自定义一个逻辑,根据需求调用setMeasuredDimension方法,所以,不同的自定义View中下面的if else逻辑可能不同:

   @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);

        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        //如果控件的width和height都设置为wrap_content,则给一个固定值,也就是这个控件默认就这么大
        //联想系统控件比如Button在设置为wrap_content时,依然会占用一定的界面尺寸
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == ·MeasureSpec.AT_MOST) {
            setMeasuredDimension(200, 200);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            //如果只有width设置为wrap_content,则height为精确值
            setMeasuredDimension(200, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {

            //如果只有height设置为wrap_content,则width为精确值
            setMeasuredDimension(widthSpecSize, 200);
        }
    }

3.重写onDraw方法

这个方法主要用于绘制界面,传入的参数是一个Canvas对象,作为整个View界面的画布,通过上一篇文章中 Android自定义View—前奏篇(Paint和Canvas的使用) 关于Paint和Canvas的使用,在这里就可以根据需求画出想要的界面

记住一点 尽量不要在onDraw方法中创建对象,会报如下警告:

这里写图片描述

因为onDraw会经常运行,频繁创建对象会很浪费内存,在onDraw中的操作越少越好!!!

当然,并不是一定不能在onDraw中创建对象,只是尽量不要。

一般情况下,Paint对象可以在构造方法中创建,这里需要注意,因为onDraw方法是频繁调用的,而Paint只创建一次,会造成一些意想不到的结果,因为Paint没有新建,上一次的Paint设置依然会在下一次的onDraw中使用。所以记得在onDraw方法中执行:

 mPaint.reset();

来重置一下Paint。

以下是一个典型的onDraw使用:

  @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        int radius = Math.min(width, height) / 2;//半径为宽和高这两个值中的最小值的一半

        canvas.drawCircle(width / 2, height / 2, radius, mPaint);//画圆

        mPaint.reset();
    }

在代码中,获取到View的宽和高,然后画了一个圆

以上,一个简单的自定义View就实现了,接下来是实例

4. 实例一:自定义渐变的SeekBar

首先上效果,如下:
这里写图片描述

看到这个可能会想到之前的Shader渲染对象,这里选择LinearGradient渲染器来实现:

整个View的思路如下:

  • 预设三种颜色值,将整个View的宽分成三段来考虑
  • 根据当前触摸屏幕的坐标,得到当前触摸在View中的位置
  • 判断当前触摸的位置在View的哪一段,然后设置不同的渲染颜色(如果在1/3内,则只有一种颜色,如果在2/3内,则有两种颜色,以此类推)

下面是实例代码:

package com.tc.customview.view;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

public class SeekBarView extends View {

    private static final int[] SECTION_COLORS = {0xffffd300, Color.GREEN, 0xff319ed4};

    private float maxCount;//SeekBar的最大值,这个是在使用View时设定
    private float currentCount;//SeekBar当前值,这个也可以在使用View时设定初始值
    private Paint mPaint;
    private int mWidth, mHeight;

    private RectF rfBase;//底层圆角矩形
    private RectF rfCover;//覆盖层圆角矩形,以上两个圆角矩形叠加构成一个圆角矩形线框

    private RectF rfContent;//内容圆角矩形,用于填充渐变颜色并根据滑动进度改变


    public SeekBarView(Context context) {
        super(context);
    }

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

    public SeekBarView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mPaint = new Paint();
        rfBase = new RectF();
        rfCover = new RectF();
        rfContent = new RectF();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);

        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);

        if (widthSpecMode == MeasureSpec.EXACTLY || widthSpecMode == MeasureSpec.AT_MOST) {
            mWidth = widthSpecSize;
        } else {
            mWidth = 0;
        }
        if (heightSpecMode == MeasureSpec.AT_MOST || heightSpecMode == MeasureSpec.UNSPECIFIED) {
            mHeight = dpToPx(15);
        } else {
            mHeight = heightSpecSize;
        }

        setMeasuredDimension(mWidth, mHeight);
    }

    private int dpToPx(int dp) {
        float scale = getContext().getResources().getDisplayMetrics().density;
        return (int) (dp * scale + 0.5f * (dp >= 0 ? 1 : -1));
    }

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

        mPaint.setAntiAlias(true);

        int round = mHeight / 2;

        //画底层的圆角矩形
        mPaint.setColor(Color.GRAY);
        rfBase.set(0, 0, mWidth, mHeight);
        canvas.drawRoundRect(rfBase, round, round, mPaint);

        //在内部画一个圆角矩形,这样两个圆角矩形重叠就形成一个圆环,也就是一个线框
        mPaint.setColor(Color.WHITE);
        rfCover.set(2, 2, mWidth - 2, mHeight - 2);
        canvas.drawRoundRect(rfCover, round, round, mPaint);



        //得到当前位置占总宽度的比例
        float section = currentCount / maxCount;

        //创建内容圆角矩形,值的设定很好理解的,就不多说了
        rfContent.set(2, 2, (mWidth - 2) * section, mHeight - 2);

        //如果当前值的比例小于等于1/3,设置颜色为颜色数组中的第一个值
        if (section <= 1.0f / 3.0f) {
            if (section != 0.0f) {
                mPaint.setColor(SECTION_COLORS[0]);
            } else {
                mPaint.setColor(Color.TRANSPARENT);
            }
        } else {
            int count = (section <= 1.0f / 3.0f * 2) ? 2 : 3;

            int[] colors = new int[count];
            System.arraycopy(SECTION_COLORS, 0, colors, 0, count);

            //下面得到的一个想对位置的颜色数组,作为LinearGradient的构造参数,如果构造参数中这个值为null,则颜色沿渐变线均匀分布
            //对此理解不够,暂时没用,按照原博写下来,先放在这里
//            float[] positions = new float[count];
//            if (count == 2) {
//                positions[0] = 0.0f;
//                positions[1] = 1.0f - positions[0];
//            } else {
//                positions[0] = 0.0f;
//                positions[1] = (maxCount / 3) / currentCount;
//                positions[2] = 1.0f - positions[0] * 2;
//            }
//            positions[positions.length - 1] = 1.0f;

            LinearGradient shader = new LinearGradient(3, 3, (mWidth - 3) * section, mHeight - 3, colors, null, Shader.TileMode.MIRROR);
            mPaint.setShader(shader);

        }
        canvas.drawRoundRect(rfContent, round, round, mPaint);

        //一次绘制完成后一定要记得重置一下画笔
        mPaint.reset();
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        getParent().requestDisallowInterceptTouchEvent(true);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                moved(x, y);
                break;
            case MotionEvent.ACTION_MOVE:
                moved(x, y);
                break;
            case MotionEvent.ACTION_UP:
                moved(x, y);
                break;
        }
        return true;
    }


    private void moved(float x, float y) {
        if (x > mWidth) {
            return;
        }
        currentCount = maxCount * (x / mWidth);
        invalidate();
    }

    public void setMaxCount(float maxCount) {
        this.maxCount = maxCount;
    }

    public void setCurrentCount(float currentCount) {
        this.currentCount = currentCount > maxCount ? maxCount : currentCount;
        invalidate();
    }

    public float getMaxCount() {
        return maxCount;
    }

    public float getCurrentCount() {
        return currentCount;
    }
}

界面布局代码:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:gravity="center"
                android:padding="16dp">

    <com.tc.customview.view.SeekBarView
        android:id="@+id/seekView"
        android:layout_width="match_parent"
        android:layout_height="20dp"/>

</RelativeLayout>

使用时的代码:

public class MainActivity extends AppCompatActivity {
    private SeekBarView seekBarView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        seekBarView = (SeekBarView) findViewById(R.id.seekBarView);
        seekBarView.setMaxCount(100);
        seekBarView.setCurrentCount(30);
    }
}

实例二:自定义闪烁文本的TextView

效果如下:
这里写图片描述

整个View的思路如下:

  • 创建的自定义View继承自TextView
  • 重写onSizeChanged方法(关于这个方法,下面会详细讲),获取到View的宽度和View的Paint,并对这个View的Paint设置渲染(到此,初始化完成了,接下来就是让这个渲染效果动起来)
  • 在onDraw方法中计算偏移量,并设置渲染的动画效果,延时刷新View。

以上是整个的思路,但是有两个新接触的东西需要说明一下:

  • onSizeChanged方法:这个方法是在TextView的大小发生变化的时候调用(如果在xml文件中对View设置为match_parent或者具体的数值,则该方法只会在初始化的时候调用一次,也就是说,只有在wrap_content的情况下,改变View的大小才会调用这个方法,当然,这个还受到父控件的宽高属性的影响,具体可以通过几个简单的测试来了解)需要注意的是,这里的大小改变并不是值TextView的文本发生改变,而是整个View的尺寸大小发生了改变,比如宽度高度发生了改变。

  • Matrix类:代码中使用到了Matrix类来实现渲染的动画效果,Android中可以给渲染器设置一个变化的Matrix,可以设置平移,旋转,缩放等动画,这里使用的是平移动画,根据一个累加的偏移量得到平移的距离,然后如下图所示可以得到想要实现的动画的偏移量的临界条件:
    这里写图片描述

把LinearGradient比作一个长方形,如上图,初始化的位置在手机屏幕的最左边,要运动到屏幕的最右边就需要2*width的长度。然后在onDraw方法中就开始计算移动的坐标,然后调用postInvalidate方法延迟去刷新界面。

接下来是实例代码:

public class TwinklingTextView extends TextView {

    private LinearGradient mLinearGradient;
    private Matrix mGradientMatrix;
    private int mViewWidth = 0;//用于获取整个View的宽度
    private int mTranslate = 0;//用于记录渲染的偏移量


    public TwinklingTextView(Context context) {
        super(context);
    }

    public TwinklingTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        //通过这个条件判断,可以保证只在初始化的时候调用一次
        if (mViewWidth == 0) {
            mViewWidth = getMeasuredWidth();
            if (mViewWidth > 0) {
                //创建渐变渲染器
                mLinearGradient = new LinearGradient(0, 0, mViewWidth, 0, new int[]{0x33ffffff, 0xffffffff, 0x33ffffff}, null, Shader.TileMode.CLAMP);

                //对当前View的paint设置渲染
                getPaint().setShader(mLinearGradient);
                mGradientMatrix = new Matrix();
            }
        }
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if (mGradientMatrix != null) {

            mTranslate += mViewWidth / 10;
            //可以看到这是一个循环的过程
            if (mTranslate > 2 * mViewWidth) {
                mTranslate = -mViewWidth;
            }
            mGradientMatrix.setTranslate(mTranslate, 0);
            mLinearGradient.setLocalMatrix(mGradientMatrix);
            postInvalidateDelayed(50);
        }
    }
}

这个例子中学习到了渲染器可以有动画的,可以对渲染器进行动画操作,这个知识点,在后面还会在用到。

实例三:自定义颜色选择器

效果如下:
这里写图片描述

思路如下:

  • 创建两个圆,一个圆环用于收集用户选择的颜色,一个实心圆用于显示用户当前选择的颜色
  • 主要在onTouch方法中实现判断用户触摸的坐标是否在圆环内,然后计算的到触摸位置相对于圆环的比例,并计算的到当前的颜色值,最后设置实心圆的颜色
  • 然后可以添加一个颜色选择的监听接口来获取选择的颜色值

本例的重点是对onTouch的操作,关于在圆环的触摸点所占比例并以此计算的到一个颜色值,下面是我总结的一点内容,可能不够准确,但是能大概有个粗浅的理解:
这里写图片描述

从图中可以看到,首先获取触摸点的坐标,然后计算得到其相对于圆心X轴的角度,通过角度又可以得到其相对于整个圆环的比例,然后根据这个比例就能计算的到当前比例位置的颜色值

下面是整个View的代码:

public class ColorPickerView extends View {

    private Paint mColorPaint;//颜色圆环的画笔
    private Paint mCenterPaint;//中心圆的画笔
    private int[] mColors;

    private OnColorChangedListener mListener;//设置颜色的回调接口

    private static final int CENTER_X = 240;
    private static final int CENTER_Y = 240;
    private static final int CENTER_RADIUS = 32;

    private static final float PI = 3.1415926f;

    public ColorPickerView(Context context) {
        super(context);
    }

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

    public ColorPickerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mColors = new int[]{//渐变色数组
                0xFFFF0000, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF, 0xFF00FF00,
                0xFFFFFF00, 0xFFFF0000};

        Shader s = new SweepGradient(0, 0, mColors, null);

        //画外部颜色选择圆环
        mColorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mColorPaint.setShader(s);
        mColorPaint.setStyle(Paint.Style.STROKE);
        mColorPaint.setStrokeWidth(32);

        //画中心的圆
        mCenterPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mCenterPaint.setColor(Color.RED);
        mCenterPaint.setStrokeWidth(15);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //设置整个界面的宽高为圆的直径大小
        setMeasuredDimension(CENTER_X * 2, CENTER_Y * 2);
    }

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

        //将画布的起始位置移动到中心位置,这样下面画圆形的时候直接设置圆心坐标为(0,0)即可
        canvas.translate(CENTER_X, CENTER_Y);

        //画外部的颜色选择环
        canvas.drawCircle(0, 0, CENTER_RADIUS * 6, mColorPaint);

        //画中心的圆形
        canvas.drawCircle(0, 0, CENTER_RADIUS, mCenterPaint);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        //获取相对于圆心的坐标
        float x = event.getX() - CENTER_X;
        float y = event.getY() - CENTER_Y;

        //判断当前触摸的位置是否在圆环内部
        boolean isInRing = Math.sqrt(x * x + y * y) <= CENTER_RADIUS;

        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:
                invalidate();
                break;

            case MotionEvent.ACTION_MOVE:
                //计算得到触摸点的角度
                float angle = (float) Math.atan2(y, x);

                //计算的到角度相对于整个圆的比例
                float unit = angle / (2 * PI);
                if (unit < 0) {
                    unit += 1;
                }
                mCenterPaint.setColor(interpColor(mColors, unit));
                invalidate();
                break;

            case MotionEvent.ACTION_UP:
                if (isInRing) {
                    mListener.colorChanged(mCenterPaint.getColor());
                }
                invalidate();
                break;
        }
        return true;
    }

    /**
     * 计算两个值之间的指定比例的值
     * 假设开始值为0,结束值为1;比例为0时,结果为0;比例为1时,结果为1;比例为0.5时,结果就是0.5
     *
     * @param s 区域的开始值
     * @param d 区域的结束值
     * @param p 比例
     * @return 四舍五入后的值
     */
    private int ave(int s, int d, float p) {
        return s + Math.round(p * (d - s));
    }


    /**
     * 根据颜色数组和当前触摸点所在的比例计算得到颜色值,这个方法暂时理解不够
     *
     * @param colors 颜色数组
     * @param unit   当前触摸点的比例
     * @return 一个颜色值
     */
    private int interpColor(int colors[], float unit) {
        if (unit <= 0) {
            return colors[0];
        }
        if (unit >= 1) {
            return colors[colors.length - 1];
        }

        float p = unit * (colors.length - 1);
        int i = (int) p;

        p -= i;

        int c0 = colors[i];
        int c1 = colors[i + 1];

        int a = ave(Color.alpha(c0), Color.alpha(c1), p);
        int r = ave(Color.red(c0), Color.red(c1), p);
        int g = ave(Color.green(c0), Color.green(c1), p);
        int b = ave(Color.blue(c0), Color.blue(c1), p);

        return Color.argb(a, r, g, b);
    }


    public interface OnColorChangedListener {
        void colorChanged(int color);
    }

    public void setOnColorChangedListener(OnColorChangedListener listener) {
        mListener = listener;
    }
}

实例四 自定义圆形颜色渐变的SeekBar

效果如下:
这里写图片描述

思路和上一个例子的颜色选择器大致相同:

  • 首先画一个圆环,作为底部背景,在其上面画一个圆弧,作为跟着手指渐变的进度条
  • 然后计算手指触摸的位置,通过角度转弧度计算,得到当前手指触摸点的弧度值
  • 将弧度值更新到渐变圆弧
    下面用一张图说明下大致过程:
    这里写图片描述

以下是完整代码:

public class CircularSeekBar extends View {

    private OnSeekChangeListener mListener;

    private Paint baseRing;//圆环背景画笔

    private RectF rect;//渐变颜色环外接矩形

    private Paint colorfulRing;//渐变颜色环画笔

    private float cx;//圆环中心位置x坐标

    private float cy;//圆环中心位置y坐标

    private float ringWidth = 30;//圆环的宽度,默认为30

    private float outerRadius;//圆环外部的半径

    private float innerRadius;//圆环内部的半径,其实就是外部半径减去圆环宽度

    private float ringRadius;//圆环的半径

    private int angle = 0;//弧度值

    private int maxProgress = 100;//最大进度值

    private int progress;

    private int progressPercent;

    private boolean CALLED_FROM_ANGLE = false;


    private static final int[] SECTION_COLORS = {0xffffd300, Color.GREEN, 0xff319ed4, 0xffffd300};


    public CircularSeekBar(Context context) {
        super(context);
    }

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

    public CircularSeekBar(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        baseRing = new Paint();
        rect = new RectF();
        colorfulRing = new Paint();

        baseRing.setColor(Color.GRAY);
        colorfulRing.setColor(Color.parseColor("#ff33b5e5"));

        baseRing.setAntiAlias(true);
        colorfulRing.setAntiAlias(true);

        baseRing.setStrokeWidth(ringWidth);
        colorfulRing.setStrokeWidth(ringWidth);

        baseRing.setStyle(Paint.Style.STROKE);
        colorfulRing.setStyle(Paint.Style.STROKE);
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int width = getWidth(); //获取控件的宽
        int height = getHeight();// 获取控件的高

        int size = (width > height) ? height : width; // 选择最小的值作为圆环视图的直径

        cx = width / 2; // 得到圆环视图的中心x坐标
        cy = height / 2; //得到圆环视图的中心y坐标
        outerRadius = size / 2; // 得到圆环外部半径

        ringRadius = outerRadius - ringWidth / 2;//得到圆环的半径

        innerRadius = outerRadius - ringWidth; // 得到圆环内部的半径

        float left = cx - ringRadius; //  渐变圆环外接矩形左边坐标
        float right = cx + ringRadius;//  渐变圆环外接矩形右边坐标
        float top = cy - ringRadius;//    渐变圆环外接矩形上边坐标
        float bottom = cy + ringRadius;// 渐变圆环外接矩形底部坐标

        rect.set(left, top, right, bottom); //设置渐变圆环的位置


    }

    @SuppressLint("DrawAllocation")
    @Override
    protected void onDraw(Canvas canvas) {

        SweepGradient shader = new SweepGradient(cx, cy, SECTION_COLORS, null);
        Matrix matrix = new Matrix();
        matrix.setRotate(-90, cx, cy);
        shader.setLocalMatrix(matrix);
        colorfulRing.setShader(shader);

        //画背景圆环
        canvas.drawCircle(cx, cy, ringRadius, baseRing);
        //画渐变圆环,每次刷新界面主要是改变这里的angle的值
        canvas.drawArc(rect, 270, angle, false, colorfulRing);
        super.onDraw(canvas);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        float x = event.getX();
        float y = event.getY();
        this.getParent().requestDisallowInterceptTouchEvent(true);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                moved(x, y, false);
                break;
            case MotionEvent.ACTION_MOVE:
                moved(x, y, false);
                break;
            case MotionEvent.ACTION_UP:
                moved(x, y, true);
                break;
        }
        return true;
    }

    private void moved(float x, float y, boolean up) {
        //计算圆心到触摸点的直线距离,使用数学中的勾股定理
        float distance = (float) Math.sqrt(Math.pow((x - cx), 2) + Math.pow((y - cy), 2));

        //如果触摸点在外圆半径的一个适配区域内
        if (distance < outerRadius + 100 && distance > innerRadius - 100 && !up) {

            //将角度转换成弧度
            float degrees = (float) ((float) ((Math.toDegrees(Math.atan2(x - cx, cy - y)) + 360.0)) % 360.0);

            //使弧度值永远为正
            if (degrees < 0) {
                degrees += 2 * Math.PI;
            }

            setAngle(Math.round(degrees));
            invalidate();

        } else {
            invalidate();
        }
    }

    public int getAngle() {
        return angle;
    }

    public void setAngle(int angle) {
        this.angle = angle;
        float donePercent = (((float) this.angle) / 360) * 100;
        float progress = (donePercent / 100) * getMaxProgress();
        setProgressPercent(Math.round(donePercent));
        CALLED_FROM_ANGLE = true;
        setProgress(Math.round(progress));
    }

    public void setSeekBarChangeListener(OnSeekChangeListener listener) {
        mListener = listener;
    }

    public OnSeekChangeListener getSeekBarChangeListener() {
        return mListener;
    }

    public float getRingWidth() {
        return ringWidth;
    }

    public void setRingWidth(float ringWidth) {
        this.ringWidth = ringWidth;
    }

    public interface OnSeekChangeListener {
        void onProgressChange(CircularSeekBar view, int newProgress);
    }

    public int getMaxProgress() {
        return maxProgress;
    }

    public void setMaxProgress(int maxProgress) {
        this.maxProgress = maxProgress;
    }

    public int getProgress() {
        return progress;
    }

    public void setProgress(int progress) {
        if (this.progress != progress) {
            this.progress = progress;
            if (!CALLED_FROM_ANGLE) {
                int newPercent = (this.progress * 100) / this.maxProgress;
                int newAngle = (newPercent * 360) / 100;
                this.setAngle(newAngle);
                this.setProgressPercent(newPercent);
            }
            if (mListener != null) {
                mListener.onProgressChange(this, this.getProgress());
            }

            CALLED_FROM_ANGLE = false;
        }
    }

    public int getProgressPercent() {
        return progressPercent;
    }

    public void setProgressPercent(int progressPercent) {
        this.progressPercent = progressPercent;
    }

    public void setRingBackgroundColor(int color) {
        baseRing.setColor(color);
    }

    public void setProgressColor(int color) {
        colorfulRing.setColor(color);
    }
}

实例五 自定义折线图

效果如下:
这里写图片描述

根据原博进行了一些修改,原博的这个例子是一个仿360流量表的例子,除了绘图外还有一些数值计算,为了简单直观的看到绘图的过程,这里我根据原博的思路,重新写了个只有一个折线图的自定义View,当然,要想实现原博的效果也不是很难,看一下源码就能理解。这个例子的步骤:

  • 画背景网格,先画竖线,再画横线,通过两个for循环就能实现
  • 画折线图,本例中折线的高度使用了一个随机数
  • 将path画会起点,根据path的使用方法,要想实现渲染效果,必须将其画回起点,形成一个封闭的折线

下面是源码:

public class LineChartView extends View {

    private Paint gridPaint;

    private Paint pathPaint;

    private Paint pathEndPaint;

    private static final int gridWidth = 45;


    public LineChartView(Context context) {
        super(context);
    }

    public LineChartView(Context context, AttributeSet attrs) {
        super(context, attrs);

        //初始化背景网格画笔
        gridPaint = new Paint();
        gridPaint.setColor(0xee000000);
        gridPaint.setStrokeWidth((float) 2.0);

        //初始化折线画笔
        pathPaint = new Paint();
        pathPaint.setAntiAlias(true);
        pathPaint.setColor(0xffcd0000);
        pathPaint.setStyle(Paint.Style.STROKE);
        pathPaint.setStrokeWidth((float) 1.0);

        //初始化折线返回路线画笔,这个用于做出渲染效果
        pathEndPaint = new Paint();
        pathEndPaint.setAntiAlias(true);
        pathEndPaint.setColor(0xffcd0000);
        pathEndPaint.setStyle(Paint.Style.FILL);

        //设置颜色渐变的渲染
        Shader shader = new LinearGradient(0, 0, gridWidth * 10, gridWidth * 20, 0xffcd0000, 0x11cd6839, Shader.TileMode.CLAMP);
        pathEndPaint.setShader(shader);
    }

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

        //画竖线
        for (int i = 0; i <= 20; i++) {
            canvas.drawLine(gridWidth * i, 0, gridWidth * i, gridWidth * 10, gridPaint);
        }

        //画横线
        for (int j = 0; j <= 10; j++) {
            canvas.drawLine(0, gridWidth * j, gridWidth * 20, gridWidth * j, gridPaint);
        }

        //画折线
        drawPath(canvas);

    }

    private void drawPath(Canvas canvas) {

        Path path = new Path();

        for (int i = 0; i <= 20; i++) {
            if (i == 0) {
                path.moveTo(0, 0);
            } else {
                //折线的高度是一个随机数
                path.lineTo(gridWidth * i, gridWidth * ((int) (Math.random() * 10)));
            }
            canvas.drawPath(path, pathPaint);
        }

        path.lineTo(gridWidth * 20, gridWidth * 10);
        path.lineTo(0, gridWidth * 10);
        path.close();
        canvas.drawPath(path, pathEndPaint);
    }
}

以上,转载内容完成,再次感谢原博,通过原博学到很多关于自定义View的知识,通过这几个例子的练手,基本掌握了自定义View的使用。

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页