这篇文章介绍了 自定义view的一些基础知识…个人总结的…
先给个效果图(没啥别的意思…本来就是练手用的…)
package com.diandou.demo41_cycle;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
/**
* Created by baiya on 2018/2/24.
*/
public class MyView extends View {
private static final String TAG = "mm_MyView";
/** 当前控件的宽 */
private int mWidthSize = 1000;
/** 当前控件的高 */
private int mHeightSize = 1000;
/** 普通画笔 */
private Paint mPaint;
/** 文字画笔 */
private TextPaint mTextPaint;
/** 路径path对象(很强大) */
private Path mPath;
/**
* 当我们 new MyView() 的时候调用...
* @param context
*/
public MyView(Context context) {
this(context, null);
}
/**
* 当我们 在xml中写这个自定义view的话会调用
* @param context
* @param attrs
*/
public MyView(Context context, AttributeSet attrs) {
super(context, attrs, 0);
init();
}
/**
* 初始化
*/
private void init() {
Log.d(TAG, "init");
/** 画笔(一般都是在构造方法中初始化滴....) */
mPaint = new Paint();
mPaint.setColor(Color.BLACK);/**设置画画笔颜色*/
mPaint.setStrokeWidth(10);/**设置画笔宽*/
/**
* Paint.Style
* 1, Paint.Style.STROKE 描边
* 2, Paint.Style.FILL 填充
* 3, Paint.Style.FILL_AND_STROKE 描边+填充
* */
mPaint.setStyle(Paint.Style.STROKE);
/**
* 这个是阴影对象, (很强大, 可以实现渐变等多种效果)
*/
// Shader shader = new Shader();
// mPaint.setShader(shader);
/** 文字画笔 */
mTextPaint = new TextPaint();
mTextPaint.setColor(Color.WHITE);
mTextPaint.setStrokeWidth(2);
mTextPaint.setStyle(Paint.Style.STROKE);/** 这个也有问题 */
mTextPaint.setTextSize(100);/** 文字大小, 文字的大小是按像素的 */
/**
* 设置文字的偏移量
* 文字的偏移量默认是left 就是 文本 左下角 的坐标点....
* */
mTextPaint.setTextAlign(Paint.Align.LEFT);
/**
* path是一个很强大的对象 + 上paint 可以绘制出任意图形:
* 1, 贝塞尔曲线
* 2, 圆
* 3, 矩形
* 4, 不规则图像
*/
mPath = new Path();
mPath.moveTo(100, 100);/** 第一个点 */
mPath.lineTo(400, 400);/** 第二个 */
mPath.lineTo(600, 200);/** 第三个 */
}
/**
* 在子view中重写onLayout方法是没有大用的其实
*
* 因为子view中没有子view了, 它不是一个父容器..
*
* onLayout 方法在viewGroup中详解...
*
* @param changed
* @param left
* @param top
* @param right
* @param bottom
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Log.d(TAG, "onLayout");
// super.onLayout(changed, left, top, right, bottom);
}
/**
* onMeasure方法被调用的时机:
*
*
* 在View里,有个mParent的变量。这个变量其实就是ViewRootImpl. 所以在调用View的requestFitSystemWindows, requestLayout, invalidateChildInParent时候,都会调用measure方法。
* (也就是说onMeasure会被它的父布局调用)
* 好吧, 具体的我也没弄清楚...
*
*
*
*
* onMeasure方法... 测量
* MeasureSpce的mode有三种:EXACTLY, AT_MOST,UNSPECIFIED
* 当父布局是EXACTLY时,子控件确定大小或者match_parent,mode都是EXACTLY,子控件是wrap_content时,mode为AT_MOST;
* 当父布局是AT_MOST时,子控件确定大小,mode为EXACTLY,子控件wrap_content或者match_parent时,mode为AT_MOST.
* 所以在确定控件大小时,需要判断MeasureSpec的mode,不能直接用MeasureSpec的size。
* 在进行一些逻辑处理以后,调用setMeasureDimension()方法,将测量得到的宽高传进去供layout使用。
*
* EXACTLY : view确定大小, 有明确的值
* 有明确的值情况: 父控件大小确定, 子控件match_parent
* 父控件大小也不确定, 但是子控件宽高 有明确值,
*
* AT_MOST : view不确定大小, 没有明确值
* 没有明确值的情况: 父控件大小不确定 , 子控件match_parent
* 父控件大小确定, 但是子控件是wrap_content 也没有明确值
*
* UNSPECIFIED: 这个不知道有什么用-_-///(一般不会是这种模式)
*
* 需要注意的是:
*
* 测量所得的宽高不一定是最后展示的宽高,最后宽高确定是在onLayout方法里,
* layou(left,top,right,bottom),不过一般都是一样的。
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.d(TAG, "onMeasure");
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
Log.d(TAG, "onMeasure" + " widthSize--" + widthSize + " heightSize--" + heightSize);
int measuredHeight, measuredWidth;
if (widthMode == MeasureSpec.EXACTLY){
measuredWidth = mWidthSize;
} else {
//如果能用到这种情况, 那就是不确定大小咯
measuredWidth = mHeightSize;
}
if (heightMode == MeasureSpec.EXACTLY) {
measuredHeight = heightSize;
} else {
measuredHeight = mWidthSize;
}
/**
* 在 setMeasuredDimension 这个方法执行之后,
* 调用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高,
* 否则得到的结果是0
*/
setMeasuredDimension(measuredWidth, measuredHeight);
}
/**
* onDraw 方法... 绘制
*
* 使用Paint画笔在canvas画布上绘制想要实现的内容...
*
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
Log.d(TAG, "onDraw");
// super.onDraw(canvas);
/**
* 画一条线 参数: x1, y1, x2, y2, 画笔
* 坐标 坐标
*/
canvas.drawLine(0,0,1000,1000, mPaint);
/**
* 平移画布...
*/
// canvas.translate(500,500);
/**
* 画一个圆 参数: x1, y1, r, 画笔
* 圆心 半径
*/
canvas.drawCircle(500, 500, 500, mPaint);
/**
* 画一段文字 参数: 文本 x1, y1, 文本画笔
* 文本左下角(和文字偏移量有关)
*/
canvas.drawText("这是一段文字", 100, 100, mTextPaint);
/**
* 矩形的类
* 构造参数: left 100, top 100, right 700, bottom 700
*/
Rect rect = new Rect(100, 100, 700, 700);
canvas.drawRect(rect, mPaint);
/**
* 和上个一样
*/
RectF rectF = new RectF(200, 200, 600, 600);
mPaint.setColor(Color.WHITE);
canvas.drawRect(rectF, mPaint);
/**
* path 很强大就是了 具体在搜博客
*/
canvas.drawPath(mPath, mPaint);
}
}
ViewGroup下的代码
package com.diandou.demo41_cycle;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
/**
* Created by baiya on 2018/2/24.
*/
public class MyViewGroup extends ViewGroup {
private static final String TAG = "mm_MyViewGroup";
public MyViewGroup(Context context) {
this(context, null);
}
public MyViewGroup(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
/**
* 初始化
*/
private void init() {
Log.d(TAG, "init");
}
/**
* 给子view排版用到的方法..
* @param changed
* @param l
* @param t
* @param r
* @param b
* 以上4个参数是在父容器的宽高, 同时也是给子view的宽高
*
*
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
Log.d(TAG, "onLayout " + l+" "+ t+" "+ r+" "+ b+" ");
int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
/** 子view */
View childView = getChildAt(i);
/**
* 发现了一个神奇的现象,
* 当我重写了onLayout用来排版子view的话, 此时, childView的onMeasure就不执行了
* */
childView.layout(r/2-500, 100, r/2+500, 1100);
}
}
@Override
protected void onDraw(Canvas canvas) {
Log.d(TAG, "onDraw");
super.onDraw(canvas);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.d(TAG, "onMeasure");
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
activity中的代码:
package com.diandou.demo41_cycle;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "mm_MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "setContentView before");
setContentView(R.layout.activity_main);
Log.d(TAG, "setContentView after");
}
@Override
protected void onResume() {
super.onResume();
Log.d(TAG, "onResume");
}
@Override
protected void onStart() {
super.onStart();
Log.d(TAG, "onStart");
}
@Override
protected void onStop() {
super.onStop();
Log.d(TAG, "onStop");
}
@Override
protected void onPause() {
super.onPause();
Log.d(TAG, "onPause");
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.d(TAG, "onDestroy");
}
}
打印的log信息
02-24 18:06:36.517 7596-7596/com.diandou.demo41_cycle D/mm_MainActivity: setContentView before
02-24 18:06:36.583 7596-7596/com.diandou.demo41_cycle D/mm_MyViewGroup: init
02-24 18:06:36.587 7596-7596/com.diandou.demo41_cycle D/mm_MyView: init
02-24 18:06:36.587 7596-7596/com.diandou.demo41_cycle D/mm_MainActivity: setContentView after
02-24 18:06:36.587 7596-7596/com.diandou.demo41_cycle D/mm_MainActivity: onStart
02-24 18:06:36.588 7596-7596/com.diandou.demo41_cycle D/mm_MainActivity: onResume
02-24 18:06:36.632 7596-7596/com.diandou.demo41_cycle D/mm_MyViewGroup: onMeasure
02-24 18:06:36.662 7596-7596/com.diandou.demo41_cycle D/mm_MyViewGroup: onMeasure
02-24 18:06:36.663 7596-7596/com.diandou.demo41_cycle D/mm_MyViewGroup: onLayout 0 0 1080 1536
02-24 18:06:36.663 7596-7596/com.diandou.demo41_cycle D/mm_MyView: onLayout
02-24 18:06:36.686 7596-7596/com.diandou.demo41_cycle D/mm_MyViewGroup: onDraw
02-24 18:06:36.686 7596-7596/com.diandou.demo41_cycle D/mm_MyView: onDraw
如果用到自定义属性的话(我个人一般不用, 写起来麻烦, 直接在代码里写不好么…):
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyView">
<attr name="background_color" format="color"/>
<attr name="size" format="dimension"/>
</declare-styleable>
</resources>
<com.diandou.demo41_cycle.MyView
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="@color/colorAccent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:background_color="@color/colorPrimary"
app:size="24dp"
/>
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyView,defStyleAttr, R.style.AppTheme);
custom_size = a.getDimensionPixelSize(R.styleable.MyView_size, 5);
custon_background = a.getColor(R.styleable.MyView_background_color, Color.BLACK);
a.recycle();
init();
}
一个简单的动画:
package com.diandou.demo41_cycle;
import android.animation.ObjectAnimator;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Created by baiya on 2018/2/24.
*/
public class MyView extends View {
private static final String TAG = "mm_MyView";
/**
* 当前控件的宽
*/
private int mWidthSize = 1000;
/**
* 当前控件的高
*/
private int mHeightSize = 1000;
/**
* 普通画笔
*/
private Paint mPaint;
/**
* 文字画笔
*/
private TextPaint mTextPaint;
/**
* 路径path对象(很强大)
*/
private Path mPath;
private int custom_size;
private int custon_background;
private ValueAnimator animator;
/**
* 当我们 new MyView() 的时候调用...
*
* @param context
*/
public MyView(Context context) {
this(context, null);
}
/**
* 当我们 在xml中写这个自定义view的话会调用
*
* @param context
* @param attrs
*/
public MyView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public MyView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyView, defStyleAttr, R.style.AppTheme);
custom_size = a.getDimensionPixelSize(R.styleable.MyView_size, 5);
custon_background = a.getColor(R.styleable.MyView_background_color, Color.BLACK);
a.recycle();
init();
}
/**
* 初始化
*/
private void init() {
Log.d(TAG, "init");
/** 画笔(一般都是在构造方法中初始化滴....) */
mPaint = new Paint();
mPaint.setColor(custon_background);/**设置画画笔颜色*/
mPaint.setStrokeWidth(custom_size);/**设置画笔宽*/
/**
* Paint.Style
* 1, Paint.Style.STROKE 描边
* 2, Paint.Style.FILL 填充
* 3, Paint.Style.FILL_AND_STROKE 描边+填充
* */
mPaint.setStyle(Paint.Style.STROKE);
/**
* 这个是阴影对象, (很强大, 可以实现渐变等多种效果)
*/
// Shader shader = new Shader();
// mPaint.setShader(shader);
/** 文字画笔 */
mTextPaint = new TextPaint();
mTextPaint.setColor(Color.WHITE);
mTextPaint.setStrokeWidth(2);
mTextPaint.setStyle(Paint.Style.STROKE);/** 这个也有问题 */
mTextPaint.setTextSize(100);/** 文字大小, 文字的大小是按像素的 */
/**
* 设置文字的偏移量
* 文字的偏移量默认是left 就是 文本 左下角 的坐标点....
* */
mTextPaint.setTextAlign(Paint.Align.LEFT);
/**
* path是一个很强大的对象 + 上paint 可以绘制出任意图形:
* 1, 贝塞尔曲线
* 2, 圆
* 3, 矩形
* 4, 不规则图像
*/
mPath = new Path();
mPath.moveTo(100, 100);/** 第一个点 */
mPath.lineTo(400, 400);/** 第二个 */
mPath.lineTo(700, 100);/** 第三个 */
mPath.lineTo(700, 700);/** 第四个 */
// Point startPoint = new Point(100, 400, 700, 700, 100, 400, 100, 700, 1);
// startPoint.setType(1);
// Point endPoint = new Point(100, 400, 700, 700, 100, 400, 100, 700, 1);
// endPoint.setType(2);
Point startPoint = new Point(100, 100);
Point endPoint = new Point(800, 800);
animator = ValueAnimator
.ofObject(new PathEvaluator(), startPoint, endPoint);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mPoint = ((Point) animation.getAnimatedValue());
// LogUtil.i(mPoint.getX()+"------"+mPoint.getY());
invalidate();
}
});
animator.setDuration(5000);
animator.start();
//
// ObjectAnimator anim = ObjectAnimator.ofObject(this, "fab", new PathEvaluator(), path.getPoints().toArray());
// anim.setInterpolator(new DecelerateInterpolator());//动画插值器
// anim.setDuration(3000);
// anim.start();
}
/**
* 在子view中重写onLayout方法是没有大用的其实
* <p>
* 因为子view中没有子view了, 它不是一个父容器..
* <p>
* onLayout 方法在viewGroup中详解...
*
* @param changed
* @param left
* @param top
* @param right
* @param bottom
*/
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Log.d(TAG, "onLayout");
// super.onLayout(changed, left, top, right, bottom);
}
/**
* onMeasure方法被调用的时机:
* <p>
* <p>
* 在View里,有个mParent的变量。这个变量其实就是ViewRootImpl. 所以在调用View的requestFitSystemWindows, requestLayout, invalidateChildInParent时候,都会调用measure方法。
* (也就是说onMeasure会被它的父布局调用)
* 好吧, 具体的我也没弄清楚...
* <p>
* <p>
* <p>
* <p>
* onMeasure方法... 测量
* MeasureSpce的mode有三种:EXACTLY, AT_MOST,UNSPECIFIED
* 当父布局是EXACTLY时,子控件确定大小或者match_parent,mode都是EXACTLY,子控件是wrap_content时,mode为AT_MOST;
* 当父布局是AT_MOST时,子控件确定大小,mode为EXACTLY,子控件wrap_content或者match_parent时,mode为AT_MOST.
* 所以在确定控件大小时,需要判断MeasureSpec的mode,不能直接用MeasureSpec的size。
* 在进行一些逻辑处理以后,调用setMeasureDimension()方法,将测量得到的宽高传进去供layout使用。
* <p>
* EXACTLY : view确定大小, 有明确的值
* 有明确的值情况: 父控件大小确定, 子控件match_parent
* 父控件大小也不确定, 但是子控件宽高 有明确值,
* <p>
* AT_MOST : view不确定大小, 没有明确值
* 没有明确值的情况: 父控件大小不确定 , 子控件match_parent
* 父控件大小确定, 但是子控件是wrap_content 也没有明确值
* <p>
* UNSPECIFIED: 这个不知道有什么用-_-///(一般不会是这种模式)
* <p>
* 需要注意的是:
* <p>
* 测量所得的宽高不一定是最后展示的宽高,最后宽高确定是在onLayout方法里,
* layou(left,top,right,bottom),不过一般都是一样的。
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.d(TAG, "onMeasure");
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
Log.d(TAG, "onMeasure" + " widthSize--" + widthSize + " heightSize--" + heightSize);
int measuredHeight, measuredWidth;
if (widthMode == MeasureSpec.EXACTLY) {
measuredWidth = mWidthSize;
} else {
//如果能用到这种情况, 那就是不确定大小咯
measuredWidth = mHeightSize;
}
if (heightMode == MeasureSpec.EXACTLY) {
measuredHeight = heightSize;
} else {
measuredHeight = mWidthSize;
}
/**
* 在 setMeasuredDimension 这个方法执行之后,
* 调用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高,
* 否则得到的结果是0
*/
setMeasuredDimension(measuredWidth, measuredHeight);
}
/**
* onDraw 方法... 绘制
* <p>
* 使用Paint画笔在canvas画布上绘制想要实现的内容...
*
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
Log.d(TAG, "onDraw");
// /**
// * 画一条线 参数: x1, y1, x2, y2, 画笔
// * 坐标 坐标
// */
// canvas.drawLine(0,0,1000,1000, mPaint);
// /**
// * 平移画布...
// */
canvas.translate(500,500);
// /**
// * 画一个圆 参数: x1, y1, r, 画笔
// * 圆心 半径
// */
// canvas.drawCircle(500, 500, 500, mPaint);
//
// /**
// * 画一段文字 参数: 文本 x1, y1, 文本画笔
// * 文本左下角(和文字偏移量有关)
// */
// canvas.drawText("这是一段文字", 100, 100, mTextPaint);
//
// /**
// * 矩形的类
// * 构造参数: left 100, top 100, right 700, bottom 700
// */
// Rect rect = new Rect(100, 100, 700, 700);
// canvas.drawRect(rect, mPaint);
//
// /**
// * 和上个一样
// */
// RectF rectF = new RectF(200, 200, 600, 600);
// mPaint.setColor(Color.WHITE);
// canvas.drawRect(rectF, mPaint);
//
// /**
// * path 很强大就是了 具体在搜博客
// */
canvas.drawPath(mPath, mPaint);
// mPath.moveTo(100, 100);/** 第一个点 */
// mPath.lineTo(400, 400);/** 第二个 */
// mPath.lineTo(700, 100);/** 第三个 */
// mPath.lineTo(700, 700);/
canvas.drawCircle(mPoint.getX(), mPoint.getY(), 50, mPaint);
}
private Point mPoint;
class Point {
float x;
float y;
public Point(float x, float y) {
this.x = x;
this.y = y;
}
public float getX() {
return x;
}
public void setX(float x) {
this.x = x;
}
public float getY() {
return y;
}
public void setY(float y) {
this.y = y;
}
}
class PathEvaluator implements TypeEvaluator<Point> {
@Override
public Point evaluate(float fraction, Point startValue, Point endValue) {
float x = fraction * (endValue.getX() - startValue.getX());
float y = fraction * (endValue.getY() - startValue.getY());
return new Point(x, y);
}
}
}
自己写的贝塞尔曲线代码:
https://www.jianshu.com/p/5ad720c77053
自定义view需要用到渐变颜色的时候(Paint.setShader):
http://blog.csdn.net/iispring/article/details/50500106
圆环上的圆点可随指尖移动
https://www.jianshu.com/p/94d858130e71
自定义view的事件分发机制
https://www.jianshu.com/p/17e3df1a1d6a
requestLayout, invalidate和postInvalidate的异同
https://www.jianshu.com/p/5c11b42ad7d0