原文地址:http://blog.csdn.net/chen_lian_/article/details/50939902
自定义View一直是横在Android开发者面前的一道坎。
一、View和ViewGroup的关系
从View和ViewGroup的关系来看,ViewGroup继承View。
View的子类,多是功能型的控件,提供绘制的样式,比如imageView,TextView等,而ViewGroup的子类,多用于管理控件的大小,位置,如LinearLayout,RelativeLayout等,从下图可以看出
从实际应用中看,他们又是组合关系,我们在布局中,常常是一个ViewGroup嵌套多个ViewGroup或View,而被嵌套的ViewGroup又会嵌套多个ViewGroup或View
如下
二、View的绘制流程
从View源码来看,主要关系三个方法:
1、measure():测量
一个final方法,控制控件的大小
2、layout():布局
用来控制自己的布局位置
有相对性,只相对于自己的父类布局,不关心祖宗布局
3、draw():绘制
用来控制控件的显示样式
流程: 流程 measure --> layout --> draw
对应于我们要实现的方法是
onMeasure()
onLayout()
onDraw()
实际绘制中,我们的思考顺序一般是这样的:
是否需要控制控件的大小-->是-->onMeasure()
(1)如果这个自定义view不是ViewGroup,onMeasure()方法调用setMeasureDeminsion(width,height):用来设置自己的大小
(2)如果是ViewGroup,onMeasure()方法调用 ,child.measure()测量孩子的大小,给出孩子的期望大小值,之后-->setMeasureDeminsion(width,height):用来设置自己的大小
是否需要控制控件的摆放位置
-->是
-->onLayout
()
是否需要控制控件的样子
-->是
-->onDraw
()-->canvas的绘制
下面是我绘制的流程图:
下面以自定义滑动按钮为例,说明自定义View的绘制流程
我们期待实现这样的效果:
拖动或点击按钮,开关向右滑动,变成
其中开关能随着手指的触摸滑动到相应位置,直到最后才固定在开关位置上
新建一个类继承自View,实现其两个构造方法
- public class SwitchButtonView extends View {
-
-
- public SwitchButtonView(Context context) {
- this(context, null);
- }
-
- public SwitchButtonView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
drawable资源中添加这两张图片
借此,我们可以用onMeasure()确定这个控件的大小
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-
- mSwitchButton = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background);
- mSlideButton = BitmapFactory.decodeResource(getResources(), R.drawable.slide_button_background);
- setMeasuredDimension(mSwitchButton.getWidth(), mSwitchButton.getHeight());
- }
这个控件并不需要控制其摆放位置,略过onLayout();
接下来onDraw()确定其形状。
但我们需要根据我们点击控件的不同行为来确定形状,这需要重写onTouchEvent()
其中的逻辑是:
当按下时,触发事件MotionEvent.Action_Down,若此时状态为关:
(1)若手指触摸点(通过event.getX()得到)在按钮的 中线右侧,按钮向右滑动一段距离(event.getX()与开关控件一半宽度之差,具体看下文源码)
(2)若手指触摸点在按钮中线左侧,按钮依旧处于最左(即“关”的状态)。
若此时状态为开:
(1)若手指触摸点在按钮中线左侧,按钮向左滑动一段距离
(2)若手指触摸点在按钮中线右侧,按钮依旧处于最右(即“开”的状态)
当滑动时,触发时间MotionEvent.Action_MOVE,逻辑与按下时一致
注意,onTouchEvent()需要设置返回值 为 Return true,否则无法响应滑动事件
当手指收起时,若开关中线位于整个控件中线左侧,设置状态为关,反之,设置为开。
具体源码如下所示:(还对外提供了一个暴露此时开关状态的接口)
自定义View部分
- package com.lian.switchtogglebutton;
-
- import android.content.Context;
- import android.graphics.Bitmap;
- import android.graphics.BitmapFactory;
- import android.graphics.Canvas;
- import android.graphics.Paint;
- import android.util.AttributeSet;
- import android.util.Log;
- import android.view.MotionEvent;
- import android.view.View;
-
-
-
-
- public class SwitchButtonView extends View {
-
- private static final int STATE_NULL = 0;
- private static final int STATE_DOWN = 1;
- private static final int STATE_MOVE = 2;
- private static final int STATE_UP = 3;
-
- private Bitmap mSlideButton;
- private Bitmap mSwitchButton;
- private Paint mPaint = new Paint();
- private int buttonState = STATE_NULL;
- private float mDistance;
- private boolean isOpened = false;
- private onSwitchListener mListener;
-
- public SwitchButtonView(Context context) {
- this(context, null);
- }
-
- public SwitchButtonView(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-
- mSwitchButton = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background);
- mSlideButton = BitmapFactory.decodeResource(getResources(), R.drawable.slide_button_background);
- setMeasuredDimension(mSwitchButton.getWidth(), mSwitchButton.getHeight());
- }
-
- @Override
- protected void onDraw(Canvas canvas) {
- super.onDraw(canvas);
- if (mSwitchButton!= null){
- canvas.drawBitmap(mSwitchButton, 0, 0, mPaint);
- }
-
- switch (buttonState){
- case STATE_DOWN:
- case STATE_MOVE:
- if (!isOpened){
- float middle = mSlideButton.getWidth() / 2f;
- if (mDistance > middle) {
- float max = mSwitchButton.getWidth() - mSlideButton.getWidth();
- float left = mDistance - middle;
- if (left >= max) {
- left = max;
- }
- canvas.drawBitmap(mSlideButton,left,0,mPaint);
- }
-
- else {
-
- canvas.drawBitmap(mSlideButton,0,0,mPaint);
- }
- }else{
- float middle = mSwitchButton.getWidth() - mSlideButton.getWidth() / 2f;
- if (mDistance < middle){
- float left = mDistance-mSlideButton.getWidth()/2f;
- float min = 0;
- if (left < 0){
- left = min;
- }
- canvas.drawBitmap(mSlideButton,left,0,mPaint);
- }else{
- canvas.drawBitmap(mSlideButton,mSwitchButton.getWidth()-mSlideButton.getWidth(),0,mPaint);
- }
- }
-
-
-
- break;
-
- case STATE_NULL:
- case STATE_UP:
- if (isOpened){
- Log.d("开关","开着的");
- canvas.drawBitmap(mSlideButton,mSwitchButton.getWidth()-mSlideButton.getWidth(),0,mPaint);
- }else{
- Log.d("开关","关着的");
- canvas.drawBitmap(mSlideButton,0,0,mPaint);
- }
- break;
-
- default:
- break;
- }
-
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent event) {
-
- switch (event.getAction()){
- case MotionEvent.ACTION_DOWN:
- mDistance = event.getX();
- Log.d("DOWN","按下");
- buttonState = STATE_DOWN;
- invalidate();
- break;
-
- case MotionEvent.ACTION_MOVE:
- buttonState = STATE_MOVE;
- mDistance = event.getX();
- Log.d("MOVE","移动");
- invalidate();
- break;
-
- case MotionEvent.ACTION_UP:
- mDistance = event.getX();
- buttonState = STATE_UP;
- Log.d("UP","起开");
- if (mDistance >= mSwitchButton.getWidth() / 2f){
- isOpened = true;
- }else {
- isOpened = false;
- }
- if (mListener != null){
- mListener.onSwitchChanged(isOpened);
- }
- invalidate();
- break;
- default:
- break;
- }
-
- return true;
- }
-
- public void setOnSwitchListener(onSwitchListener listener){
- this.mListener = listener;
- }
-
- public interface onSwitchListener{
- void onSwitchChanged(boolean isOpened);
- }
- }
DemoActivity:
- package com.lian.switchtogglebutton;
-
- import android.os.Bundle;
- import android.support.v7.app.AppCompatActivity;
- import android.widget.Toast;
-
- public class MainActivity extends AppCompatActivity {
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
-
- SwitchButtonView switchButtonView = (SwitchButtonView) findViewById(R.id.switchbutton);
- switchButtonView.setOnSwitchListener(new SwitchButtonView.onSwitchListener() {
- @Override
- public void onSwitchChanged(boolean isOpened) {
- if (isOpened) {
- Toast.makeText(MainActivity.this, "打开", Toast.LENGTH_SHORT).show();
- }else {
- Toast.makeText(MainActivity.this, "关闭", Toast.LENGTH_SHORT).show();
- }
- }
- });
- }
- }
布局:
- <?xml version="1.0" encoding="utf-8"?>
- <RelativeLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:paddingBottom="@dimen/activity_vertical_margin"
- android:paddingLeft="@dimen/activity_horizontal_margin"
- android:paddingRight="@dimen/activity_horizontal_margin"
- android:paddingTop="@dimen/activity_vertical_margin"
- tools:context="com.lian.switchtogglebutton.MainActivity">
-
- <com.lian.switchtogglebutton.SwitchButtonView
- android:id="@+id/switchbutton"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- />
- </RelativeLayout>