通常来说自定义view可以分为两种方法,一种是继承自某个已有控件,如TextView,Button等,还有一种办法就是通过继承View来自绘控件。这里主要讲一下第二种办法。
view的绘制过程主要分为三个步骤:measure,layout和draw。在自定义view的过程中,一般需要对measure和draw过程进行重写,即重写onMeasure和onDraw方法。
onMeasure(int widthMeasureSpec, int heightMeasureSpec)
正如方法名字所述,onMeasure主要进行的是对自定义view的测量,获得view的宽度和高度,这里给定的两个参数widthMeasureSpec和heightMeasureSpec对应的就是宽和高的元数据,这两个通常是由父视图(ViewGroup)传过来的。这里还是要大致介绍一下MeasureSpec这个参数。
MeasureSpec
MeasureSpec是一个int类型的数据,其中最高的两位代表了mode,剩下的30位的值代表了size。这里所说的mode就是我们通常在.xml文件中定义的layout_width和layout_height了。mode可以分为三种类型:EXACTLY (子view有具体数值),AT_MOST (父view对子view最大值进行限定,一般子view的LayoutParams为wrap_content),UNSPECIFIED (子view可以是随意大小)。当时这样的解释还是不太清楚,不过这篇文章不准备讲源码所以就不深入分析了。
这里要注意的一点就是,对于我们通用的view,当我们将其LayoutParams设置为wrap_content时,view会有一个对应的大小,但是对于我们自定义的view,如果设置为wrap_content的话,会发现它的宽高与父view是一样的。这是由于当我们设置为wrap_content时,MeasureSpec的size部分的值为父view的大小,如果不进行改变,会导致子view的宽高与父view相同。所以一般在onMeasure的时候要对MeasureSpec中的size部分进行相应处理:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mWidth = getMySize(50,widthMeasureSpec);
mHeight = getMySize(50,heightMeasureSpec);
setMeasuredDimension(mWidth,mHeight);
}
private int getMySize(int defaultSize, int measureSpec){
int mySize = defaultSize;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
switch(mode){
//修改为给定的默认值
case MeasureSpec.UNSPECIFIED:
mySize = defaultSize;
break;
//修改为给定的默认值
case MeasureSpec.AT_MOST:
mySize = defaultSize;
break;
case MeasureSpec.EXACTLY:
mySize = size;
break;
}
return mySize;
}
当然我们也可以进行其他的修改,例如对应view的宽必须与屏幕宽度相同之类的。
onDraw(Canvas canvas)
这里主要的操作就是自行定义想要绘制的图形了。这里需要注意的一点就是,这里传入的canvas的宽和高,是我们在onMeasure的时候获得的宽和高。所以如果我们在onDraw的时候改变主意想要画更大的图,结果就是最终只能展示出来一部分图像。假设我们要画两个同心圆,就可以绘制如下:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();
int wid = getWidth() - paddingLeft - paddingRight;
int hei = getHeight() - paddingTop - paddingBottom;
int radius = Math.min(wid,hei)/2;
//Log.d(TAG, "radius: " + radius);
int centerX = paddingLeft + radius;
int centerY = paddingTop + radius;
mPaint.setColor(mOutsideColor);
canvas.drawCircle(centerX,centerY,radius,mPaint);
mPaint.setColor(mInsideColor);
canvas.drawCircle(centerX,centerY,radius*0.75f,mPaint);
}
xml中设置如下:
<com.example.xuyueqing.mycusview.XyqCusView.MyView
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_margin="5dp"
app:OutsideColor="@color/colorGray"
app:InsideColor="@color/colorBlue"
/>
效果如下:
不过对于这种简单的圆形之类的,不建议自己去自定义个view...继承一个TextView然后在.xml中设置一下背景,也能够出来相同的效果。
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@drawable/double_circle"
/>
<!--drawable.double_circle.xml-->
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<size
android:width="80dp"
android:height="80dp">
</size>
<solid android:color="@color/colorBlue"></solid>
<stroke
android:width="10dp"
android:color="@color/colorGray"></stroke>
</shape>
自定义style
在使用自定义view的时候,还有一个点就是有时候需要有一些自定义属性,例如同心圆中内圆和外圆的颜色,这个需要我们在styles.xml中设置declare-styleable来进行属性配置。
<!-- styles.xml -->
<declare-styleable name="MyView">
<attr name="InsideColor" format="color" />
<attr name="OutsideColor" format="color"/>
</declare-styleable>
在自定义view的constructor中对这两种属性进行获取:
//在java代码中new
public MyView(Context context) {
super(context);
mContext = context;
}
//在.xml中声明
public MyView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mContext = context;
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MyView);
//第二个参数为默认值
mOutsideColor = ta.getColor(R.styleable.MyView_OutsideColor,Color.BLACK);
mInsideColor = ta.getColor(R.styleable.MyView_InsideColor,Color.BLUE);
//记得recycle,防止内存泄漏
ta.recycle();
}
这样就可以为自定义的view配置属性了。
自定义进度条
最后贴一个自定义的进度条的代码,大致的样式如下:
等下次弄个动图...主要实现的功能就是:中间按钮可以拖动,随着拖动中间的数字会改变,且按钮左右的线的颜色不一样,这个相对比较复杂的view就只能自己来定义了。代码中基本把上面提到的自定义view所涉及的点都用上了,顺便监听了touch事件。
代码如下:
public class MySeekBar extends View {
//bar状态改变监听
public interface MySeekBarChangeListener {
void ProgressChanged(MySeekBar mySeekBar, int progress);
}
private static final String TAG = "MySeekBar";
//当前位置
private float mCurIndex;
private Paint mLeftLinePaint, mRightLinePaint, mRectPaint;
private Rect mBound;
private Paint mTextPaint;
private int mScreenWidth;
private int mScreenHeight;
//整个进度条大小
private int mViewWidth;
private int mViewHeight;
//中间按钮属性
private int mRectWidth;
private int mRectHeight;
private int mRectColor;
//文字属性
private float mTextSize;
private int mTextColor;
//左右线属性
private float mLeftLineWidth;
private float mRightLineWidth;
private int mLeftLineColor;
private int mRightLineColor;
//整个view的中间位置
private int mCenterY;
//起始点和终止点
private int mStartX;
private int mEndX;
//进度条的进度值
private int progress;
private List<MySeekBarChangeListener> mSeekBarChangeListenerList;
public MySeekBar(Context context) {
super(context);
init();
}
/**
* .xml中定义
*/
public MySeekBar(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
//获取.xml中定义的属性
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.MySeekBar);
mLeftLineColor = ta.getColor(R.styleable.MySeekBar_leftLineColor, Color.MAGENTA);
mRightLineColor = ta.getColor(R.styleable.MySeekBar_rightLineColor, Color.GRAY);
mLeftLineWidth = ta.getDimension(R.styleable.MySeekBar_leftLineWidth, UIUtils.dp2px(context, 2));
mRightLineWidth = ta.getDimension(R.styleable.MySeekBar_rightLineWidth, UIUtils.dp2px(context, 1));
mTextSize = ta.getDimension(R.styleable.MySeekBar_textSize, UIUtils.dp2px(context, 15));
mTextColor = ta.getColor(R.styleable.MySeekBar_textColor, Color.WHITE);
mRectColor = ta.getColor(R.styleable.MySeekBar_rectColor, Color.BLACK);
ta.recycle();
init();
}
public MySeekBar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mViewHeight = getMySize(50, heightMeasureSpec);
mViewWidth = MeasureSpec.getSize(widthMeasureSpec);
setMeasuredDimension(mViewWidth, mViewHeight);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//获取中心点
mCenterY = mViewHeight / 2;
//画左右的线
if (mStartX < mCurIndex - mRectWidth / 2) {
canvas.drawLine(mStartX, mCenterY, mCurIndex - mRectWidth / 2, mCenterY, mLeftLinePaint);
}
if (mCurIndex + mRectWidth / 2 < mEndX) {
canvas.drawLine(mCurIndex + mRectWidth / 2, mCenterY, mEndX, mCenterY, mRightLinePaint);
}
//画中间按钮
mRectPaint.setColor(mRectColor);
mRectPaint.setStyle(Paint.Style.FILL);//充满
mRectPaint.setAntiAlias(true);// 设置画笔的锯齿效果
RectF roundRect = new RectF(mCurIndex - mRectWidth / 2, mCenterY - mRectHeight / 2, mCurIndex + mRectWidth - mRectWidth / 2, mCenterY - mRectHeight / 2 + mRectHeight);
canvas.drawRoundRect(roundRect, 40, 20, mRectPaint);
//画中间的text
mRectPaint.setColor(mTextColor);
String text = String.valueOf(progress);
mRectPaint.getTextBounds(text, 0, text.length(), mBound);
canvas.drawText(text, mCurIndex - mBound.width() / 2, mCenterY + mBound.height() / 2, mRectPaint);
}
/**
* 监听touch动作
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
if (this.isEnabled()) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
//修改进度条状态
case MotionEvent.ACTION_MOVE:
mCurIndex = event.getX();
changeStatus();
break;
case MotionEvent.ACTION_UP:
mCurIndex = event.getX();
changeStatus();
break;
default:
break;
}
return true;
}
return false;
}
private int getMySize(int defaultSize, int measureSpec) {
int mySize = defaultSize;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
switch (mode) {
case MeasureSpec.UNSPECIFIED:
mySize = defaultSize;
break;
case MeasureSpec.AT_MOST:
mySize = defaultSize;
break;
case MeasureSpec.EXACTLY:
mySize = size;
break;
}
return mySize;
}
/**
* 初始化状态
*/
private void init() {
mSeekBarChangeListenerList = new ArrayList<>();
mLeftLinePaint = new Paint();
mLeftLinePaint.setAntiAlias(true);
mLeftLinePaint.setColor(mLeftLineColor);
mLeftLinePaint.setStrokeWidth(mLeftLineWidth);
mRightLinePaint = new Paint();
mRightLinePaint.setAntiAlias(true);
mRightLinePaint.setColor(mRightLineColor);
mRightLinePaint.setStrokeWidth(mRightLineWidth);
mRectPaint = new Paint();
mRectPaint.setAntiAlias(true);
mRectPaint.setTextSize(mTextSize);
mBound = new Rect();
mRectPaint.getTextBounds("100", 0, 3, mBound);
mRectWidth = mBound.width() + 40;
mRectHeight = mBound.height() + 30;
mCurIndex = 0;
mScreenWidth = getResources().getDisplayMetrics().widthPixels;
mScreenHeight = getResources().getDisplayMetrics().heightPixels;
mStartX = mRectWidth / 2;
mCurIndex = mRectWidth / 2;
mEndX = mScreenWidth - mRectWidth / 2;
}
/**
* 修改进度条状态
*/
private void changeStatus() {
mCurIndex = Math.max(mCurIndex, mRectWidth / 2);
mCurIndex = Math.min(mCurIndex, (mViewWidth - mRectWidth + mRectWidth / 2));
int curProgress = (int) ((mCurIndex - mRectWidth / 2) / (mViewWidth - mRectWidth) * 100.0f);
//进度值变化
if (curProgress != progress) {
progress = curProgress;
invalidate();
onProgressRefresh();
}
}
public void registerSeekBarChangeListener(MySeekBarChangeListener m) {
mSeekBarChangeListenerList.add(m);
}
public void unregisterSeekBarChangeListener(MySeekBarChangeListener m) {
mSeekBarChangeListenerList.remove(m);
}
/**
* 修改listener状态
*/
private void onProgressRefresh() {
if (!mSeekBarChangeListenerList.isEmpty()) {
for (MySeekBarChangeListener mySeekBarChangeListener : mSeekBarChangeListenerList) {
mySeekBarChangeListener.ProgressChanged(this, progress);
}
}
}
}
.xml中配置如下:
<com.example.xuyueqing.mycusview.XyqCusView.MySeekBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
app:leftLineColor="@color/colorPink"
app:rectColor="@color/colorBlack80"
app:rightLineColor="@color/colorGray"
app:textColor="@color/colorWhite" />
<!-- styles.xml -->
<declare-styleable name="MySeekBar">
<attr name="leftLineColor" format="color" />
<attr name="rightLineColor" format="color" />
<attr name="rectColor" format="color" />
<attr name="textColor" format="color" />
<attr name="leftLineWidth" format="dimension" />
<attr name="rightLineWidth" format="dimension" />
<attr name="textSize" format="dimension" />
</declare-styleable>