Android学习笔记17-自定义控件

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值