本文转自 http://blog.csdn.net/jiangwei0910410003/article/details/42640665 稍有修改,感谢分享!
考虑到篇幅和内容相关性,将其分为两篇文章,这里主要记录自定义View的具体使用
接上文 Android自定义View—前奏篇(Paint和Canvas的使用)
自定义View的流程为:
public LabelView(Context context, AttributeSet attrs)
定义属性
也就是自定义View的各种属性,可以在xml文件中使用的属性,比如color,textSize等
重写onMeasure方法(非必须)
用于计算View在屏幕中的大小
重写onDraw方法
用于在屏幕上绘制自定义View这里主要用到Paint和Canvas
重写onTouch方法
用于获取对屏幕的触摸事件,创建能够与用户交互的自定义View
在开始自定义View之前,先总结下自定义属性,很多时候自定义View都需要用到自定义属性。
在values目录下创建自定义属性的attrs.xml文件(一般为这个文件名,当然也可以是其他命名):
这样就定义好了一个自定义属性集合“CircleView”,这个集合的名字随便起,在自定义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();//释放资源
}
}
"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"/>
关于申明命名空间,只需要保证在根布局申明的命名空间前缀“app”和自定义View中使用属性时的前缀一致就行了,当然是可以随便起名的,开心就好。
到此,自定义属性的定义,设置和调用就完成了。
一般来说,自定义控件都会去重写View的onMeasure方法,因为该方法指定该控件在屏幕上的大小。
protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec)
onMeasure传入的两个参数是由上一层控件传入的大小,有多种情况,重写该方法时需要计算控件的实际大小,然后调用setMeasuredDimension(int, int)设置实际大小。
onMeasure传入的widthMeasureSpec和heightMeasureSpec不是一般的尺寸数值,而是将模式和尺寸组合在一起的数值。
获取模式:
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);
}
}
这个方法主要用于绘制界面,传入的参数是一个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就实现了,接下来是实例
看到这个可能会想到之前的Shader渲染对象,这里选择LinearGradient渲染器来实现:
整个View的思路如下:
下面是实例代码:
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;
}
}
界面布局代码:
"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"/>
使用时的代码:
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);
}
}
整个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的操作,关于在圆环的触摸点所占比例并以此计算的到一个颜色值,下面是我总结的一点内容,可能不够准确,但是能大概有个粗浅的理解:
从图中可以看到,首先获取触摸点的坐标,然后计算得到其相对于圆心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;
}
}
思路和上一个例子的颜色选择器大致相同:
以下是完整代码:
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,当然,要想实现原博的效果也不是很难,看一下源码就能理解。这个例子的步骤:
下面是源码:
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的使用。