要自定义控件需要先继承View然后再重写两个方法,分别是onDraw方法和onMeasure().
(有时还可能会用到onLayout方法—改变该自定义控件在ViewGroup中的位置)
(加载自定义属性和的layout布局文件的代码可写在构造方法中,findviewbyId实例化控件写在onFinishInflate方法中更好些)
(在某些情况下还会用到onSizeChanged方法,view的绘图触发事件可写在这里。)
如果只重写onFinishInflate(),onSizeChanged(),onDraw()三个方法时执行顺序是:
22:23:03.597: D/mDebug(9715): onFinishInflate
22:23:03.667: D/mDebug(9715):onSizeChanged,w=240,h=282,oldw=0,oldh=0
22:23:03.727: D/mDebug(9715): onDraw
//onDraw的参数是canvas: the canvas on which the background will be drawn
onDraw方法:主要负责绘制图形(也可是Bitmap)
注意哦:该控件继承的对象是,直接或,间接继承View的ViewGroup,比如LinearLayout,可能onDraw不会被执行,这是就要注意,setWillNotDraw(false);setWillNotDraw===>让不绘图,false。双重否定,就是让他绘图,之后onDraw才会被调用。
onMeasure方法主要是负责控制该view的大小(没有重写时是默认调用系统的onMeasure方法)。
有个没意义的小实验:有时候控件不想被xml中写死,想在代码中动态的改变控件的大小(只是外部大小,内容没有进行缩放,设置之后部分内容可能被遮掉),
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int height = MeasureSpec.getSize(heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
heightMeasureSpec = MeasureSpec.makeMeasureSpec(height,MeasureSpec.UNSPECIFIED);
widthMeasureSpec = MeasureSpec.makeMeasureSpec(width,MeasureSpec.UNSPECIFIED);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
在onMeasure方法中加上这段代码,将测量模式设置为MeasureSpec.UNSPECIFIED,也就是大小不受限制,Java代码中可以通过setMinimumHeight()和setMinimumWidth()动态的修改控件大小。
相当于在外部调用了本来外部不能被调用到的setMeasuredDimension,控制控件的大小。
进入正题,其实开关就是两个图片构成的,一张是背景图片一张是开罐图片,只需要控制开关图片,就能显示的看到开关的改变。就是通过监听用户对控件(其实就是背景图片上面的开关图片)对它的滑动或点击事件。
实现方式可能有多种,有些方式区分多种状态标记,且点击(DOWN)后立即响应开关滑至左边或右边。而这里没有用多种状态标记,且是对点击事件(DOWN)不立刻处理,而是在松开后才会移动到左边或右边,滑动事件是立即处理,移动的时候一直在重绘。
如果是直接通过代码添加那就需要测量出那张图drawable_bg的大小作为
控件的总宽度bgbitmap.getWidth(),高度也是一样的,
但是如果想添加在xml布局中那就需要自定义属性,然后通过typedArray
获取到用户在xml中设置的drawable,得到那张图片的width和height.作为总的大小,相比之下xml中自定义属性(背景图,开关图,开关的状态灵活性更高)可以动态的替换控件的背景图和开关图,
这里有个麻烦的地方,自定义属性得到的是Drawable,而控件的canvas只能化能画Bitmap,所以需要将Drawable转成Bitmap。方法有两种。
方法一:
public Bitmap DrawableConventToBitmap(Drawable drawable) {
// 创建一个空的Bitmap 里面是参数Drawable的大小信息
// Drawable--->创建Bitmap
Bitmap bitmap = Bitmap.createBitmap(
drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight(),
drawable.getOpacity()!=PixelFormat.OPAQUE?
Config.ARGB_8888:Bitmap.Config.RGB_565
);
// 依据这Bitmap再创建一个canvas
// Bitmap--->创建Canvas
Canvas canvas = new Canvas(bitmap);
// 为drawable设置一个大小,当drawable调用onDraw方法时就会去在这个范围里面去draw
// Drawable宽高属性getIntrinsic--->Drawable.setBound()
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
// 往canvas里面画,范围大小就是上一部设置的SetBound的大小
// canvas--->draw()
drawable.draw(canvas);
return bitmap;
}
方法二:
Bitmap bitmap = null;
BitmapDrawable bd = (BitmapDrawable) drawable;
bitmap = bd.getBitmap();
return bitmap;
1.画出图片,开关的背景图片和开关图片,背景图片在开关图的下面。
背景图片从开关的左上角开始画起也就是(0,0)坐标,开关图片要注意需要从控件的(0,size/2)开始画起,
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawBitmap(bgbitmap, 0, 0, paint); // 在左上角开始绘制
// 在宽:bgbitmapWidth左边绘制,高bgbitmapHeight的一般(相当于垂直居中)
canvas.drawBitmap(switchbitmap, changeposition,
(bgbitmap.getHeight() - switchbitmap.getHeight()) / 2, paint);
}
2,控制自定义view的大小,在onMeasure里面写:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(bgbitmap.getWidth(), bgbitmap.getHeight());
}
3 实现onTouchEvent方法或者OnTouchListener接口都行.在onDraw里面画开关图片的时候预留一个参数changeposition(x轴上的坐标)去
出发事件后调用invalidate();通知view去重绘一下。
注意:开关的触发点在滑动开关图片的中间。所以所有的点击的位置需要减去开关图的一半
startposi = event.getX() - switchbitmap.getWidth() / 2;
Down事件的时候只需要记录开关图的x位置
Move事件就是实时的获取到x位置然后再去调用invalidate()
UP事件主要是判断当前位置是在左边还是右边,判断之后直接调用invalidate(),让它绘制到左边或者右边。
事件判别的代码:
@Override
public boolean onTouchEvent(MotionEvent event) {
float startposi = 0;
float stopPointx = 0;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 记录点下的x位置
startposi = event.getX() - switchbitmap.getWidth() / 2;
System.out.println("stopPointx-->" + stopPointx);
break;
case MotionEvent.ACTION_MOVE:
stopPointx = event.getX() - switchbitmap.getWidth() / 2;
// 因为是想在switchbitmap的中间触发
// 所以画图开始的位置是从所点击的位置要减去 switchbitmap/2的距离
changeposition = (int) stopPointx;
if (changeposition >= bgbitmap.getWidth() - switchbitmap.getWidth()) {
changeposition = bgbitmap.getWidth() - switchbitmap.getWidth();
} else if (changeposition < 0) {
changeposition = 0;
}
invalidate();
// 重新刷新起始点
startposi = event.getX() - switchbitmap.getWidth() / 2;
break;
case MotionEvent.ACTION_UP:
float now = event.getX() - switchbitmap.getWidth() / 2;
if (now >= bgbitmap.getWidth() / 2 - switchbitmap.getWidth() / 2) {
changeposition = bgbitmap.getWidth() - switchbitmap.getWidth();
isSwitch = true;
switchListener.onSwitch(true);
} else {
changeposition = 0;
isSwitch = false;
switchListener.onSwitch(false);
}
invalidate();
break;
default:
break;
}
return true;
}
控件自定义完成,但是使用起来还需要给外部留接口操作,对不同状态进行不同操作。设计接口回调。
步骤1:
创建接口,以及接口中的方法
interface onSwitchListener {
public void onSwitch(boolean toggleState);
}
步骤二:
创建接口对象
private onSwitchListener switchListener;
步骤三:
给外部public方法设置接口对象
public void setOnSwitchListener(onSwitchListener switchListener) {
this.switchListener = switchListener;
}
步骤四:
在该类中合适的地方调用接口中的方法
case MotionEvent.ACTION_UP:
float now = event.getX() - switchbitmap.getWidth() / 2;
if (now >= bgbitmap.getWidth() / 2 - switchbitmap.getWidth() / 2) {
changeposition = bgbitmap.getWidth() - switchbitmap.getWidth();
isSwitch = true;
switchListener.onSwitch(true);
} else {
changeposition = 0;
isSwitch = false;
switchListener.onSwitch(false);
}
invalidate();
break;
default:
break;
使用时外部需要set接口或者是implements接口,并且实现接口中的方法。
public class MainActivity extends AppCompatActivity implements onSwitchListener {
private CustomSwitchView switchview;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
switchview = (CustomSwitchView) findViewById(R.id.customswitch);
switchview.setOnSwitchListener(this);
}
@Override
public void onSwitch(boolean toggleState) {
if (toggleState == true) {
Toast.makeText(MainActivity.this, "开了", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this, "关了", Toast.LENGTH_SHORT).show();
}
}
}