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;
    }
}

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

点击下载源码

目录
相关文章
|
11月前
|
Android开发 UED 计算机视觉
Android自定义view之线条等待动画(灵感来源:金铲铲之战)
本文介绍了一款受游戏“金铲铲之战”启发的Android自定义View——线条等待动画的实现过程。通过将布局分为10份,利用`onSizeChanged`测量最小长度,并借助画笔绘制动态线条,实现渐变伸缩效果。动画逻辑通过四个变量控制线条的增长与回退,最终形成流畅的等待动画。代码中详细展示了画笔初始化、线条绘制及动画更新的核心步骤,并提供完整源码供参考。此动画适用于加载场景,提升用户体验。
670 5
Android自定义view之线条等待动画(灵感来源:金铲铲之战)
|
11月前
|
Android开发
Android自定义view之利用PathEffect实现动态效果
本文介绍如何在Android自定义View中利用`PathEffect`实现动态效果。通过改变偏移量,结合`PathEffect`的子类(如`CornerPathEffect`、`DashPathEffect`、`PathDashPathEffect`等)实现路径绘制的动态变化。文章详细解析了各子类的功能与参数,并通过案例代码展示了如何使用`ComposePathEffect`组合效果,以及通过修改偏移量实现动画。最终效果为一个菱形图案沿路径运动,源码附于文末供参考。
208 0
|
11月前
|
XML Java Android开发
Android自定义view之网易云推荐歌单界面
本文详细介绍了如何通过自定义View实现网易云音乐推荐歌单界面的效果。首先,作者自定义了一个圆角图片控件`MellowImageView`,用于绘制圆角矩形图片。接着,通过将布局放入`HorizontalScrollView`中,实现了左右滑动功能,并使用`ViewFlipper`添加图片切换动画效果。文章提供了完整的代码示例,包括XML布局、动画文件和Java代码,最终展示了实现效果。此教程适合想了解自定义View和动画效果的开发者。
457 65
Android自定义view之网易云推荐歌单界面
|
11月前
|
XML 前端开发 Android开发
一篇文章带你走近Android自定义view
这是一篇关于Android自定义View的全面教程,涵盖从基础到进阶的知识点。文章首先讲解了自定义View的必要性及简单实现(如通过三个构造函数解决焦点问题),接着深入探讨Canvas绘图、自定义属性设置、动画实现等内容。还提供了具体案例,如跑马灯、折线图、太极图等。此外,文章详细解析了View绘制流程(measure、layout、draw)和事件分发机制。最后延伸至SurfaceView、GLSurfaceView、SVG动画等高级主题,并附带GitHub案例供实践。适合希望深入理解Android自定义View的开发者学习参考。
839 84
|
11月前
|
前端开发 Android开发 UED
讲讲Android为自定义view提供的SurfaceView
本文详细介绍了Android中自定义View时使用SurfaceView的必要性和实现方式。首先分析了在复杂绘制逻辑和高频界面更新场景下,传统View可能引发卡顿的问题,进而引出SurfaceView作为解决方案。文章通过Android官方Demo展示了SurfaceView的基本用法,包括实现`SurfaceHolder.Callback2`接口、与Activity生命周期绑定、子线程中使用`lockCanvas()`和`unlockCanvasAndPost()`方法完成绘图操作。
302 3
|
移动开发 前端开发 Java
Android 进阶路线(思维导图)
Android 进阶路线(思维导图)
Android 进阶路线(思维导图)
|
6月前
|
移动开发 前端开发 Android开发
【02】建立各项目录和页面标准化产品-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
【02】建立各项目录和页面标准化产品-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
1073 12
【02】建立各项目录和页面标准化产品-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
|
6月前
|
移动开发 JavaScript 应用服务中间件
【06】优化完善落地页样式内容-精度优化-vue加vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
【06】优化完善落地页样式内容-精度优化-vue加vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
838 5
【06】优化完善落地页样式内容-精度优化-vue加vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
|
6月前
|
移动开发 Rust JavaScript
【01】首页建立-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
【01】首页建立-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
1006 4
【01】首页建立-vue+vite开发实战-做一个非常漂亮的APP下载落地页-支持PC和H5自适应提供安卓苹果鸿蒙下载和网页端访问-优雅草卓伊凡
|
7月前
|
开发工具 Android开发
X Android SDK file not found: adb.安卓开发常见问题-Android SDK 缺少 `adb`(Android Debug Bridge)-优雅草卓伊凡
X Android SDK file not found: adb.安卓开发常见问题-Android SDK 缺少 `adb`(Android Debug Bridge)-优雅草卓伊凡
771 11
X Android SDK file not found: adb.安卓开发常见问题-Android SDK 缺少 `adb`(Android Debug Bridge)-优雅草卓伊凡