1、View绘制的基本概念:
我们所有的控件都是继承View这个类的,View的创建主要有3个步骤
measure(测量设置布局的大小) -> layout(设置布局的位置) -> draw(绘制)
在这个3个步骤中系统对应有一些回调函数,我们可以在这个回调函数中做我们自己的事。
onMeasure -> onLayout -> onDraw
我们在layout布局中加载我们自己定义的控件需要注意:
a.必须要指定width 和height,因为你不指定就通不过编译,虽然你设置了也没用。
b.自定义的布局一定要加完整的路径,例如:
<com.example.myswitch.MySwitch
android:id="@+id/ms_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
注意:我们在布局预览中也可看到我们定义的布局哦。
2、自定义view步骤:
a. 写类继承View
b. 重写onDraw, 进行绘制
c. 重新onMeasure,修改尺寸
d. 在xml布局文件中配置
简单的代码演示:(简单的画个矩形)
public class MySwitch extends View {
private Paint mPaint;//定义个全局的画笔
//系统调用的,这个构造在加载我们xml文件会调用,所以一定要重写这个构造
public MySwitch(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView();
}
//系统调用的,这个构造在加载我们xml文件会调用,所以一定要重写这个构造
public MySwitch(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
//这个是我们程序员调用的,如果你不会手动去new这个布局,也可以不用重写这个构造。
public MySwitch(Context context) {
super(context);
initView();
}
//我们不用每次在draw的时候去new 画笔,浪费内存,所以定义个全局的变量
private void initView() {
// 初始化画笔
mPaint = new Paint();
mPaint.setColor(Color.RED);// 画笔颜色
}
// 设置尺寸回调
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// super.onMeasure(widthMeasureSpec, heightMeasureSpec);//查看源码,也是调用setMeasuredDimension
// 将当前控件宽高设置为100x100,就类似画布设置100*100,你draw的矩形比这个大也没用
setMeasuredDimension(100, 100);
}
// measure->layout->draw
// onMeasure->onLayout->onDraw
@Override
protected void onDraw(Canvas canvas) {
// 绘制200x200的矩形
canvas.drawRect(0, 0, 200, 200, mPaint);
System.out.println("onDraw");
}
}
这样我们一个简单的自定义View就实现了。
3、自定义一个开关按钮:
在上面的onMeasure方法中我们直接设置了 100*100 可能会导致布局里面的东西显示不全
我们就需要动态的设置宽高了
setMeasuredDimension(mBitmapBg.getWidth(), mBitmapBg.getHeight());// 依据背景图片来确定控件大小
代码演示:
public class MySwitch extends View {
private Paint mPaint;
private Bitmap mBitmapBg;
private Bitmap mBitmapSlide;
private int MAX_LEFT;// 滑块最大左边距
private int mSlideLeft;// 当前左边距
private boolean isOpen;// 当前开关状态
public MySwitch(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initView();
}
public MySwitch(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
// 加载自定义滑块图片
int slideId = attrs.getAttributeResourceValue(NAMESPACE, "slide", -1);
if (slideId > 0) {
mBitmapSlide = BitmapFactory.decodeResource(getResources(), slideId);
}
}
public MySwitch(Context context) {
super(context);
initView();
}
private void initView() {
// 初始化画笔
mPaint = new Paint();
mPaint.setColor(Color.RED);// 画笔颜色
// 初始化背景bitmap
mBitmapBg = BitmapFactory.decodeResource(getResources(),R.drawable.switch_background);
// 初始化滑块bitmap
mBitmapSlide = BitmapFactory.decodeResource(getResources(),R.drawable.slide_button);
MAX_LEFT = mBitmapBg.getWidth() - mBitmapSlide.getWidth();
this.setOnClickListener(new OnClickListener() {//在整个view设置点击侦听
@Override
public void onClick(View v) {
if (isClick) {//解决点击和滑动的冲突
if (isOpen) {
isOpen = false;// 关闭开关
mSlideLeft = 0;
} else {
isOpen = true;// 打开开关
mSlideLeft = MAX_LEFT;
}
invalidate();// view重绘的方法, 刷新view, 重新调用onDraw方法
if (mListener != null) {// 回调当前开关状态
mListener.onCheckChanged(MySwitch.this, isOpen);
}
}
}
});
}
int startX = 0;
int moveX = 0;// 位移距离
boolean isClick;// 标记当前是触摸还是单击事件
@Override
public boolean onTouchEvent(MotionEvent event) {//重写方法,实现滑动切换
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 1. 记录起始点x坐标
startX = (int) event.getX();// 获取相对于当前控件的x坐标
break;
case MotionEvent.ACTION_MOVE:
int endX = (int) event.getX();// 2. 记录移动后的x坐标
int dx = endX - startX;// 3. 记录x偏移量
mSlideLeft += dx;// 4. 根据偏移量,更新mSlideLeft
moveX += Math.abs(dx);// 向左向右移动都要统计下来, 所以要用dx绝对值
if (mSlideLeft < 0) {// 避免滑块超出边界
mSlideLeft = 0;
}
if (mSlideLeft > MAX_LEFT) {// 避免滑块超出边界
mSlideLeft = MAX_LEFT;
}
invalidate();// 5. 刷新界面
startX = (int) event.getX();// 6. 重新初始化起始点坐标
break;
case MotionEvent.ACTION_UP:
if (moveX < 5) {// 根据位移判断是单击事件还是移动事件
isClick = true;// 单击事件,小于5个像素认为就是点击
} else {
isClick = false;// 移动事件,大于5个像素就认为是滑动
}
moveX = 0;// 初始化移动的总距离,不然下次会出错
if (!isClick) {//判断是滑动了
if (mSlideLeft < MAX_LEFT / 2) {// 根据当前位置, 切换开关状态
mSlideLeft = 0;// 关闭开关
isOpen = false;
} else {
mSlideLeft = MAX_LEFT;// 打开开关
isOpen = true;
}
invalidate();
if (mListener != null) {// 回调当前开关状态
mListener.onCheckChanged(MySwitch.this, isOpen);
}
}
break;
}
return super.onTouchEvent(event);//返回true就消耗了事件,这个是为了解决滑动和点击事件冲突
}
@Override// 设置尺寸回调
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(mBitmapBg.getWidth(), mBitmapBg.getHeight());// 依据背景图片来确定控件大小
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawBitmap(mBitmapBg, 0, 0, null);// 绘制背景图片
canvas.drawBitmap(mBitmapSlide, mSlideLeft, 0, null);// 绘制滑块图片
}
private OnCheckChangeListener mListener;
// 设置开关状态监听
public void setOnCheckChangeListener(OnCheckChangeListener listener) {
mListener = listener;
}
//监听开关状态的回调接口
public interface OnCheckChangeListener {
public void onCheckChanged(View view, boolean isChecked);
}
}
注意:
invalidate()方法是让view失效,如果view是可见的状态,就会立即调用ondraw方法。
在解决点击和滑动事件冲突的时候,我们是通过滑动的距离来判断的。
4、设置自定义View的属性:
例如我们可以直接在xml中设置开关的属性来控制第一次显示的是开还是关,图片的样式。
步骤:
a.在res-values的attr.xml文件输入,没有就新建一个:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MySwitch">
<attr name="slide" format="reference" />//滑块的属性
<attr name="checked" format="boolean" />//开关的状态
</declare-styleable>
</resources>
b.在xml中使用:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<!-- 将android替换为我们的包名-->
xmlns:mynamespace="http://schemas.android.com/apk/res/com.example.myswitch"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<com.mynamespace.myswitch.MySwitch
android:id="@+id/ms_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
mynamespace:checked="true"
mynamespace:slide="@drawable/slide_button" />
</RelativeLayout>
注意:使用我们自定义的属性没有代码提示功能
c.在我们定义的View修改下代码:
加一个全局变量
private static final String NAMESPACE = "http://schemas.android.com/apk/res/com.example.myswitch";
修改下构造函数,只需要改这个系统调用的就行
public MySwitch(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
// 获取属性值
isOpen = attrs.getAttributeBooleanValue(NAMESPACE, "checked", false);
// 加载自定义滑块图片
int slideId = attrs.getAttributeResourceValue(NAMESPACE, "slide", -1);
if (slideId > 0) {//如果指定了滑块的背景图就加载
mBitmapSlide = BitmapFactory.decodeResource(getResources(), slideId);
}
if (isOpen) {
mSlideLeft = MAX_LEFT;
} else {
mSlideLeft = 0;
}
invalidate();//刷新下界面
}
这样我们就可以在xml文件中直接设置开关的属性啦。
比较全的开源自定义控件
https://github.com/Trinea/android-open-project/
https://github.com/lightSky/Awesome-MaterialDesign