今天我们要实现的是是一个自定义开关,之前都是集成已有的控件,今天这个却有些不太一样,这个是继承View的,也就真的是完全自定义控件了
public class ToggleView extends View {
public ToggleView(Context context) {
super(context);
}
public ToggleView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ToggleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
OK,解释下这三个构造方法:
接下来先简单说下Android的绘制流程,大体说下:
Android绘制控件在界面打开时,生命周期onResume()之后
measure -> layout -> draw
对应的就是 测量 -> 摆放 ->绘制
对应的方法就是 onMeasure -> onLayout -> onDraw重写这些方法,实现自定义控件
其中:
View只需要两个方法:
onMeasure()(指定自己的宽高) -> onDraw()(绘制自己的内容)
ViewGroup需要三个方法:
onMeasure()(指定自己的宽高,所有子View的宽高) ->onLayout()(摆放所有子View) -> onDraw()(绘制自己的内容)
OK,之后我们把我们自定义的控件先添加到住布局中,这里注意,要全路经带包名的
"@+id/toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
然后找到它,并且给它设置一些属性,没有的方法我们在我们自定义控件中添加旧OK了
mToggleView = (ToggleView) findViewById(R.id.toggle);
//设置背景
mToggleView.setSwitchBackgroundResource(R.mipmap.switch_background);
//设置滑块背景
mToggleView.setSlideButtonResource(R.mipmap.slide_button);
//设置开关状态
mToggleView.setSwitchState(false);
设置背景了,我们就可以把它转成一个Bitmap对象
//设置背景
public void setSwitchBackgroundResource(int background) {
mSwitchBackgroundResouce = BitmapFactory.decodeResource(getResources(),background);
}
//设置滑块背景
public void setSlideButtonResource(int button) {
mSlideButtonBitmap = BitmapFactory.decodeResource(getResources(),button);
}
接下来我们就要重写那两个方法了onMeasure()和onDraw()
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
这里我们只需要显示我们开关的宽高就可以了,所以我们得修改下onMeasure的返回值,返回刚才我们设置的背景的宽高就行了
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(mSwitchBackgroundResouce.getWidth(),mSwitchBackgroundResouce.getHeight());
}
接下里就到onDraw方法了,Canvas 是画布的意思,我们通过它就可以绘制出我们想要的图形,油画布了还需要一只画笔,画笔可以共用,所以我们在每个构造方法都加一个init()方法,初始化画笔,同时以后也要初始化一些其它的东西
private void init() {
mPaint = new Paint();
}
有了画布和画笔就开始绘制我们的控件了:
第一步:画背景
canvas.drawBitmap(mSwitchBackgroundResouce,0,0,mPaint);
第二步:画滑块
这步的注意下,因为开关的滑块不是固定的,开关有两种状态,在不同的状态下,滑块的位置是不同的,同样画的也是不同的,这个状态值就是我们之前传过来的boolean值,接下来我们就要通过这个值来进行绘制
public void setSwitchState(boolean state) {
this.mSwitchState=state;//给状态值赋值
}
有了状态值就开始画滑块:
//根据开关状态直接设置图片位置
if (mSwitchState){
int newLeft=mSwitchBackgroundResouce.getWidth()-mSlideButtonBitmap.getWidth();//获取左边的坐标
canvas.drawBitmap(mSlideButtonBitmap,newLeft,0,mPaint);
}else{
canvas.drawBitmap(mSlideButtonBitmap,0,0,mPaint);
}
OK,控件基本已经画出来了,现在我们点击它还不会动,因为缺少触摸事件的监听,所以接下来我们就要给它设置触摸监听,因为我们是自定义的控件,我们就直接在我们的自定义控件内重写方法了,外面不需要管里面是怎么实现的
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
currentX = event.getX();//获取当前位置
break;
case MotionEvent.ACTION_MOVE:
currentX = event.getX();//获取当前位置
break;
case MotionEvent.ACTION_UP:
currentX = event.getX();//获取当前位置
break;
}
return true;
}
我们之前都是都过一个boolean值来画滑块的,但是实际中我们却要根据用户的滑动位置来画滑块,所以这里我们要设置一个boolean值,表示当前是否是滑动状态,我们按下去的时候置为true,表示滑动状态,松手置为false,表示不是滑动状态了,同时还要重绘界面,当我们处在滑动状态时,我们就按照滑动状态绘制滑块;当我们不在滑动状态时,我们还是要按照之前开关状态绘制滑块
说下重绘界面方法:
// 会引发onDraw()被调用, 里边的变量会重新生效.界面会更新
invalidate();
贴下完整的onDraw()方法:
// Canvas 画布, 画板. 在上边绘制的内容都会显示到界面上.
@Override
protected void onDraw(Canvas canvas) {
// 1. 绘制背景
canvas.drawBitmap(switchBackgroupBitmap, 0, 0, paint);
// 2. 绘制滑块
if(isTouchMode){
// 根据当前用户触摸到的位置画滑块
// 让滑块向左移动自身一半大小的位置
float newLeft = currentX - slideButtonBitmap.getWidth() / 2.0f;
int maxLeft = switchBackgroupBitmap.getWidth() - slideButtonBitmap.getWidth();
// 限定滑块范围
if(newLeft < 0){
newLeft = 0; // 左边范围
}else if (newLeft > maxLeft) {
newLeft = maxLeft; // 右边范围
}
canvas.drawBitmap(slideButtonBitmap, newLeft, 0, paint);
}else {
// 根据开关状态boolean, 直接设置图片位置
if(mSwitchState){// 开
int newLeft = switchBackgroupBitmap.getWidth() - slideButtonBitmap.getWidth();
canvas.drawBitmap(slideButtonBitmap, newLeft, 0, paint);
}else {// 关
canvas.drawBitmap(slideButtonBitmap, 0, 0, paint);
}
}
}
boolean isTouchMode = false;
// 重写触摸事件, 响应用户的触摸.
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
isTouchMode = true;
currentX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
currentX = event.getX();
break;
case MotionEvent.ACTION_UP:
isTouchMode = false;
currentX = event.getX();
float center = switchBackgroupBitmap.getWidth() / 2.0f;
// 根据当前按下的位置, 和控件中心的位置进行比较.
boolean state = currentX > center;
break;
default:
break;
}
// 重绘界面
invalidate(); // 会引发onDraw()被调用, 里边的变量会重新生效.界面会更新
return true; // 消费了用户的触摸事件, 才可以收到其他的事件.
}
OK,接下来我么还差一个接口监听,类似于View的setOnClickListener一样,当我们的开关状态改变时我们得需要通知外面,这里就是通过接口回调来实现的
首先,在我们自定义的类中定义一个接口,同时提供set方法
public interface OnSwitchStateUpdateListener{
// 状态回调, 把当前状态传出去
void onStateUpdate(boolean state);
}
public void setOnSwitchStateUpdateListener(
OnSwitchStateUpdateListener onSwitchStateUpdateListener) {
this.onSwitchStateUpdateListener = onSwitchStateUpdateListener;
}
OK,那么我们在哪里调用呢?没错,就是在我们松开手指的时候,我们就要把状态值传出去:
case MotionEvent.ACTION_UP:
isTouchMode = false;
System.out.println("event: ACTION_UP: " + event.getX());
currentX = event.getX();
float center = switchBackgroupBitmap.getWidth() / 2.0f;
// 根据当前按下的位置, 和控件中心的位置进行比较.
boolean state = currentX > center;
// 如果开关状态变化了, 通知界面. 里边开关状态更新了.
if(state != mSwitchState && onSwitchStateUpdateListener != null){
// 把最新的boolean, 状态传出去了
onSwitchStateUpdateListener.onStateUpdate(state);
}
mSwitchState = state;
break;
OK,这样我们就可以在MainActivity中拿到这个状态值了
// 设置开关更新监听
mToggleView.setOnSwitchStateUpdateListener(new OnSwitchStateUpdateListener(){
@Override
public void onStateUpdate(boolean state) {
Toast.makeText(getApplicationContext(), "state: " + state, 0).show();
}
});
好了,这样看来整体上我们的这个控件好像写完了,其实没有,因为一般自定义控件设置属性都不会在代码中设置的,就是MainActivity中那几行代码,一般都是在xml中配置这些,所以我们也要这么做.
首先,我们要在res目录下的values目录下新建一个attrs.xml,之后我们就可以声明我们的自定义属性了
"1.0" encoding="utf-8"?>
"ToggleView">
"switch_background" format="reference" />
"slide_button" format="reference" />
"switch_state" format="boolean" />
这里有兴趣的童鞋可以看下Android的一些源码,比如View的,为什么有些控件我们在xml中能直接使用一些属性,因为Android已经帮我们定义好了一些属性,定义的格式和我们上面那段一样的
OK,声明完我们需要的属性后,我们就可以在xml中使用了,怎么使用呢?首先需要我们自己命名一个命名空间,名字随意,然后就可以使用了:
"1.0" encoding="utf-8"?>
"http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.hfs.toggleview.MainActivity">
"@+id/toggle"
android:layout_centerInParent="true"
app:switch_background="@mipmap/switch_background"
app:slide_button="@mipmap/slide_button"
app:switch_state="false"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
这里的app就是我们自己的命名空间
这样我们就可以把那三行代码删除了,同时也可以删除我们自定义类中对应的方法了,于是MainActivity代码就很简单了
public class MainActivity extends AppCompatActivity {
private ToggleView mToggleView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mToggleView = (ToggleView) findViewById(R.id.toggle);
mToggleView.setOnSwitchStateUpdateListener(new ToggleView.onSwitchStateUpdateListener() {
@Override
public void onStateUpdate(boolean state) {
if (state) {
Toast.makeText(MainActivity.this, "开关打开", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this, "开关关闭", Toast.LENGTH_SHORT).show();
}
}
});
}
}
OK,这样我们的一个简单自定义开关就写完了,这只是个例子,起到抛砖引用作用
源码:
https://github.com/Greathfs/ToggleView