Android进阶之绘制-自定义View完全掌握(四)

简介: Android进阶之绘制-自定义View完全掌握(四)

前面的案例中我们都是使用系统的一些控件通过组合的方式来生成我们自定义的控件,自定义控件的实现还可以通过自定义类继承View来完成。从该篇博客开始,我们通过自定义类继承View来实现一些我们自定义的控件。
我们通过一个案例来学习,现在来实现这样一个效果。
在这里插入图片描述
我们新建一个类MyToggleButton,让它继承View。
注意,一定要重写带两个参数的构造方法,因为如果我们在布局文件使用该类,将会用这个构造方法实例该类,如果没有就崩溃。
介绍一下一个控件从创建到显示过程中的主要方法。

  1. 执行构造方法实例化类
  2. 测量,通过measure方法,需要去重写onMeasure方法

如果当前是一个ViewGroup,它还有义务去测量它的孩子
孩子只有建议权,就是说孩子可以建议控件多高多宽,而最后是必须父类去决定宽高的

  1. 指定位置,通过layout方法,需要去重写onLayout方法

指定控件的位置,一般View不用重写该方法,只有是ViewGroup的时候才需要去重写它

  1. 绘制视图,通过draw方法,需要去重写onDraw方法

根据上面两个方法的一些参数进行绘制

所以我们自定义View一般只需要重写onMeasure(int,int)方法和onDraw(canvas)方法。
基本操作由三个方法完成:measure()方法、layou()方法、draw()方法,其内部又分别包含了onMeasure()方法、onLayout()方法、onDraw()方法。
贴出MyToggleButton类的代码。

package com.itcast.test0430_2;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

import butterknife.BindBitmap;

/**
 * Created by Administrator on 2019/4/30 0030.
 */

public class MyToggleButton extends View {

    private Bitmap backgroundBitmap;

    private Bitmap slidingBitmap;

    private int slidLeftMax;
    private Paint paint;

    /**
     *  如果我们在布局文件使用该类,将会用这个构造方法实例该类,如果没有就崩溃
     * @param context
     * @param attrs
     */
    public MyToggleButton(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    private void initView() {
        paint = new Paint();
        paint.setAntiAlias(true);//设置抗锯齿
        backgroundBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.switch_background);
        slidingBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.switch_button);
        slidLeftMax = backgroundBitmap.getWidth() - slidingBitmap.getWidth();
    }

    /**
     * 视图的测量
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(backgroundBitmap.getWidth(),backgroundBitmap.getHeight());
    }

    /**
     * 绘制
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(backgroundBitmap,0,0,paint);
        canvas.drawBitmap(slidingBitmap,0,0,paint);
    }
}

通过上面的讲述,相信这些代码你们都理解。这样一个自定义的View就绘制好了,然后我们在activity_main.xml文件中使用。

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.itcast.test0430_2.MainActivity">

    <com.itcast.test0430_2.MyToggleButton
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true" />

</RelativeLayout>

运行项目,预览效果。
在这里插入图片描述
这样一个静态的开关就被绘制上去了,现在我们要让开关通过点击能改变状态。
我们先来分析一下,现在的状态是处于关闭的状态,如何让它处于开启状态?我们在绘制第二张图的时候是距离左边距为0,而此时我们已经计算出了开启状态需要距离左边的边距,所以,我们只需这样修改

canvas.drawBitmap(slidingBitmap,slidLeftMax,0,paint);

即可,我们重新运行项目,预览效果。
在这里插入图片描述
这样就使得开关处于开启的状态了。既然如此,那我们就可以通过动态地改变左边距的值从而间接地控制开关状态。
我们重新修改MyToggleButton类的代码。

package com.itcast.test0430_2;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

import butterknife.BindBitmap;

/**
 * Created by Administrator on 2019/4/30 0030.
 */

public class MyToggleButton extends View implements View.OnClickListener {

    private Bitmap backgroundBitmap;
    private Bitmap slidingBitmap;
    /**
     * 距离左边的最大距离
     */
    private int slidLeftMax;
    private Paint paint;
    private int slideLeft;

    /**
     * 如果我们在布局文件使用该类,将会用这个构造方法实例该类,如果没有就崩溃
     *
     * @param context
     * @param attrs
     */
    public MyToggleButton(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    private void initView() {
        paint = new Paint();
        paint.setAntiAlias(true);//设置抗锯齿
        backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background);
        slidingBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_button);
        slidLeftMax = backgroundBitmap.getWidth() - slidingBitmap.getWidth();

        setOnClickListener(this);
    }

    /**
     * 视图的测量
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(backgroundBitmap.getWidth(), backgroundBitmap.getHeight());
    }

    /**
     * 绘制
     *
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(backgroundBitmap, 0, 0, paint);
        canvas.drawBitmap(slidingBitmap, slideLeft, 0, paint);
    }

    private boolean isOpen = false;

    @Override
    public void onClick(View v) {
        isOpen = !isOpen;

        if (isOpen) {
            slideLeft = slidLeftMax;
        } else {
            slideLeft = 0;
        }
        //强制绘制
        invalidate();//这个方法会导致onDraw()方法执行
    }
}

这样我们就完成了点击,运行项目,预览效果。
在这里插入图片描述
但是,这离我们的目标还是有一点距离的,我们继续来实现下一个需求,开关的滑动。
要想实现这样的需求,我们就需要去重写onTouchEvent()方法来监听触摸事件,然后获得按下时的坐标,但是在event对象中,有getX()方法和getRawX()方法,那么我们应该使用哪个方法呢?这两个方法有什么区别呢?
我贴出两张图。
在这里插入图片描述在这里插入图片描述
相信看到图就一目了然了吧。
我们对MyToggleButton类的代码进行修改。

package com.itcast.test0430_2;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import butterknife.BindBitmap;

/**
 * Created by Administrator on 2019/4/30 0030.
 */

public class MyToggleButton extends View implements View.OnClickListener {

    private Bitmap backgroundBitmap;
    private Bitmap slidingBitmap;
    /**
     * 距离左边的最大距离
     */
    private int slidLeftMax;
    private Paint paint;
    private int slideLeft;

    /**
     * 如果我们在布局文件使用该类,将会用这个构造方法实例该类,如果没有就崩溃
     *
     * @param context
     * @param attrs
     */
    public MyToggleButton(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    private void initView() {
        paint = new Paint();
        paint.setAntiAlias(true);//设置抗锯齿
        backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background);
        slidingBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_button);
        slidLeftMax = backgroundBitmap.getWidth() - slidingBitmap.getWidth();

        setOnClickListener(this);
    }

    /**
     * 视图的测量
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(backgroundBitmap.getWidth(), backgroundBitmap.getHeight());
    }

    /**
     * 绘制
     *
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(backgroundBitmap, 0, 0, paint);
        canvas.drawBitmap(slidingBitmap, slideLeft, 0, paint);
    }

    private boolean isOpen = false;

    @Override
    public void onClick(View v) {
        isOpen = !isOpen;

        if (isOpen) {
            slideLeft = slidLeftMax;
        } else {
            slideLeft = 0;
        }
        //强制绘制
        invalidate();//这个方法会导致onDraw()方法执行
    }

    private float startX;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                //1、记录按下的坐标
                startX = event.getX();
                break;
            case MotionEvent.ACTION_MOVE:
                //2、记录结束值
                float endX = event.getX();
                //3、计算偏移量
                float distanceX = endX - startX;

                slideLeft += distanceX;
                //4、屏蔽非法值

                //5、刷新
                invalidate();
                //6、数据还原
                startX = event.getX();

                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return super.onTouchEvent(event);
    }
}

现在运行项目,预览效果。
在这里插入图片描述
会发现,开关竟然被滑出去了,显然这种现象是不被允许的,我们把第四步屏蔽非法值实现一下。

 if(slideLeft < 0){
       slideLeft = 0;
 }else if(slideLeft > slidLeftMax){
      slideLeft = slidLeftMax;
 }

现在运行预览。
在这里插入图片描述
现在我们已经无法将开关滑出控件外,但是,不知道你们有没有发现,它可以滑动到一个比较尴尬的地方,就是既不是开启状态,也不是关闭状态,而是处于两者中间,那这种情况同样也是不被允许的,所以,我们现在来解决一下这个问题。
重新修改MyToggleButton类的代码。

package com.itcast.test0430_2;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import butterknife.BindBitmap;

/**
 * Created by Administrator on 2019/4/30 0030.
 */

public class MyToggleButton extends View implements View.OnClickListener {

    private Bitmap backgroundBitmap;
    private Bitmap slidingBitmap;
    /**
     * 距离左边的最大距离
     */
    private int slidLeftMax;
    private Paint paint;
    private int slideLeft;

    /**
     * 如果我们在布局文件使用该类,将会用这个构造方法实例该类,如果没有就崩溃
     *
     * @param context
     * @param attrs
     */
    public MyToggleButton(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    private void initView() {
        paint = new Paint();
        paint.setAntiAlias(true);//设置抗锯齿
        backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background);
        slidingBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_button);
        slidLeftMax = backgroundBitmap.getWidth() - slidingBitmap.getWidth();

        setOnClickListener(this);
    }

    /**
     * 视图的测量
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(backgroundBitmap.getWidth(), backgroundBitmap.getHeight());
    }

    /**
     * 绘制
     *
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(backgroundBitmap, 0, 0, paint);
        canvas.drawBitmap(slidingBitmap, slideLeft, 0, paint);
    }

    private boolean isOpen = false;

    @Override
    public void onClick(View v) {
        isOpen = !isOpen;

        flushView();
    }

    private void flushView() {
        if (isOpen) {
            slideLeft = slidLeftMax;
        } else {
            slideLeft = 0;
        }
        //强制绘制
        invalidate();//这个方法会导致onDraw()方法执行
    }

    private float startX;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                //1、记录按下的坐标
                startX = event.getX();
                break;
            case MotionEvent.ACTION_MOVE:
                //2、记录结束值
                float endX = event.getX();
                //3、计算偏移量
                float distanceX = endX - startX;

                slideLeft += distanceX;

                //4、屏蔽非法值
                if(slideLeft < 0){
                    slideLeft = 0;
                }else if(slideLeft > slidLeftMax){
                    slideLeft = slidLeftMax;
                }

                //5、刷新
                invalidate();
                //6、数据还原
                startX = event.getX();

                break;
            case MotionEvent.ACTION_UP:
                if(slideLeft > slidLeftMax / 2){
                    //显示按钮开
                    isOpen = true;
                }else{
                    isOpen = false;
                }

                flushView();
                break;
        }
        return true;
    }
}

运行项目,预览效果。
在这里插入图片描述
这个时候,虽然不会出现上次的尴尬情况,但是,这里又有一个问题,就是我在滑动的时候,它总是往我滑动的反方向跑,我想让它向右滑动,可它偏偏就要去左边,这显然也是不行的吧。这是因为我们的触摸事件和点击事件同时作用产生的问题。我们现在来解决这个问题。
再次修改MyToggleButton类的代码。

package com.itcast.test0430_2;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import butterknife.BindBitmap;

/**
 * Created by Administrator on 2019/4/30 0030.
 */

public class MyToggleButton extends View implements View.OnClickListener {

    private Bitmap backgroundBitmap;
    private Bitmap slidingBitmap;
    /**
     * 距离左边的最大距离
     */
    private int slidLeftMax;
    private Paint paint;
    private int slideLeft;

    /**
     * 如果我们在布局文件使用该类,将会用这个构造方法实例该类,如果没有就崩溃
     *
     * @param context
     * @param attrs
     */
    public MyToggleButton(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    private void initView() {
        paint = new Paint();
        paint.setAntiAlias(true);//设置抗锯齿
        backgroundBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_background);
        slidingBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.switch_button);
        slidLeftMax = backgroundBitmap.getWidth() - slidingBitmap.getWidth();

        setOnClickListener(this);
    }

    /**
     * 视图的测量
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(backgroundBitmap.getWidth(), backgroundBitmap.getHeight());
    }

    /**
     * 绘制
     *
     * @param canvas
     */
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(backgroundBitmap, 0, 0, paint);
        canvas.drawBitmap(slidingBitmap, slideLeft, 0, paint);
    }

    private boolean isOpen = false;
    /**
     * true:点击事件生效,滑动事件不生效
     * false:点击事件不生效,滑动事件生效
     */
    private boolean isEnableClick = true;

    @Override
    public void onClick(View v) {
        if (isEnableClick) {
            isOpen = !isOpen;
            flushView();
        }
    }

    private void flushView() {
        if (isOpen) {
            slideLeft = slidLeftMax;
        } else {
            slideLeft = 0;
        }
        //强制绘制
        invalidate();//这个方法会导致onDraw()方法执行
    }

    private float startX;
    private float lastX;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //1、记录按下的坐标
                lastX = startX = event.getX();
                isEnableClick = true;
                break;
            case MotionEvent.ACTION_MOVE:
                //2、记录结束值
                float endX = event.getX();
                //3、计算偏移量
                float distanceX = endX - startX;

                slideLeft += distanceX;

                //4、屏蔽非法值
                if (slideLeft < 0) {
                    slideLeft = 0;
                } else if (slideLeft > slidLeftMax) {
                    slideLeft = slidLeftMax;
                }

                //5、刷新
                invalidate();
                //6、数据还原
                startX = event.getX();

                if (Math.abs(endX - lastX) > 5) {
                    //滑动
                    isEnableClick = false;
                }

                break;
            case MotionEvent.ACTION_UP:
                if (!isEnableClick) {
                    if (slideLeft > slidLeftMax / 2) {
                        //显示按钮开
                        isOpen = true;
                    } else {
                        isOpen = false;
                    }
                    flushView();
                }
                break;
        }
        return true;
    }
}

这就是我们的最终版代码,也就完成了整个的案例。

点击下载源码

目录
相关文章
|
1月前
|
数据可视化 Android开发 开发者
安卓应用开发中的自定义View组件
【10月更文挑战第5天】在安卓应用开发中,自定义View组件是提升用户交互体验的利器。本篇将深入探讨如何从零开始创建自定义View,包括设计理念、实现步骤以及性能优化技巧,帮助开发者打造流畅且富有创意的用户界面。
74 0
|
12天前
|
搜索推荐 前端开发 Android开发
安卓应用开发中的自定义视图实现
【10月更文挑战第30天】在安卓开发的海洋中,自定义视图是那抹不可或缺的亮色,它为应用界面的个性化和交互体验的提升提供了无限可能。本文将深入探讨如何在安卓平台创建自定义视图,并展示如何通过代码实现这一过程。我们将从基础出发,逐步引导你理解自定义视图的核心概念,然后通过一个实际的代码示例,详细讲解如何将理论应用于实践,最终实现一个美观且具有良好用户体验的自定义控件。无论你是想提高自己的开发技能,还是仅仅出于对安卓开发的兴趣,这篇文章都将为你提供价值。
|
14天前
|
Android开发 开发者 UED
安卓开发中自定义View的实现与性能优化
【10月更文挑战第28天】在安卓开发领域,自定义View是提升应用界面独特性和用户体验的重要手段。本文将深入探讨如何高效地创建和管理自定义View,以及如何通过代码和性能调优来确保流畅的交互体验。我们将一起学习自定义View的生命周期、绘图基础和事件处理,进而探索内存和布局优化技巧,最终实现既美观又高效的安卓界面。
27 5
|
21天前
|
缓存 数据处理 Android开发
在 Android 中使用 RxJava 更新 View
【10月更文挑战第20天】使用 RxJava 来更新 View 可以提供更优雅、更高效的解决方案。通过合理地运用操作符和订阅机制,我们能够轻松地处理异步数据并在主线程中进行 View 的更新。在实际应用中,需要根据具体情况进行灵活运用,并注意相关的注意事项和性能优化,以确保应用的稳定性和流畅性。可以通过不断的实践和探索,进一步掌握在 Android 中使用 RxJava 更新 View 的技巧和方法,为开发高质量的 Android 应用提供有力支持。
|
21天前
|
缓存 调度 Android开发
Android 在子线程更新 View
【10月更文挑战第21天】在 Android 开发中,虽然不能直接在子线程更新 View,但通过使用 Handler、AsyncTask 或 RxJava 等方法,可以实现子线程操作并在主线程更新 View 的目的。在实际应用中,需要根据具体情况选择合适的方法,并注意相关的注意事项和性能优化,以确保应用的稳定性和流畅性。可以通过不断的实践和探索,进一步掌握在子线程更新 View 的技巧和方法,为开发高质量的 Android 应用提供支持。
30 2
|
22天前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
|
26天前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
24 2
|
1月前
|
XML 前端开发 Java
安卓应用开发中的自定义View组件
【10月更文挑战第5天】自定义View是安卓应用开发的一块基石,它为开发者提供了无限的可能。通过掌握其原理和实现方法,可以创造出既美观又实用的用户界面。本文将引导你了解自定义View的创建过程,包括绘制技巧、事件处理以及性能优化等关键步骤。