前面的案例中我们都是使用系统的一些控件通过组合的方式来生成我们自定义的控件,自定义控件的实现还可以通过自定义类继承View来完成。从该篇博客开始,我们通过自定义类继承View来实现一些我们自定义的控件。

我们通过一个案例来学习,现在来实现这样一个效果。



我们新建一个类MyToggleButton,让它继承View。

注意,一定要重写带两个参数的构造方法,因为如果我们在布局文件使用该类,将会用这个构造方法实例该类,如果没有就崩溃。

介绍一下一个控件从创建到显示过程中的主要方法。

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

    如果当前是一个ViewGroup,它还有义务去测量它的孩子

    孩子只有建议权,就是说孩子可以建议控件多高多宽,而最后是必须父类去决定宽高的
  3. 指定位置,通过layout方法,需要去重写onLayout方法

    指定控件的位置,一般View不用重写该方法,只有是ViewGroup的时候才需要去重写它
  4. 绘制视图,通过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;
}
}

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

点击下载源码

Android进阶之绘制-自定义View完全掌握(四)的更多相关文章

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

    这是自定义View系列的第二篇博客,我们继续来学习关于自定义View的知识. 今天我们来实现一下广告条案例. 我们要实现的是这样的一个效果. 要想实现这样的效果,我们可以借助ViewPager控件,然 ...

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

    Android的UI设计可以说是决定一个app质量的关键因素,因为人们在使用app的时候,最先映入眼帘的就是app的界面了,一个美观.充实的界面能够给用户带来非常好的体验,会在用户心中留下好的印象. ...

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

    自定义View系列的第三篇博客,我们来学习如何实现自定义下拉框. 今天的程序,我们来实现这样的一个效果. 布局非常简单,我们直接开始编码. 修改activity_main.xml文件的代码. < ...

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

    在自定义类继承View实现自定义控件的过程中,我们还应该对一些自定义属性有所了解. 我们通过一个案例来学习一下. 新建一个android项目,然后我们创建一个类MyAttributeView继承Vie ...

  5. Android进阶(二十)AndroidAPP开发问题汇总(四)

    · Android进阶(二十)AndroidAPP开发问题汇总(四) android:layout_width和android:width的区别 基中的android:layout_width和and ...

  6. Android显示框架:自定义View实践之绘制篇

    文章目录 一 View 二 Paint 2.1 颜色处理 2.2 文字处理 2.3 特殊处理 三 Canvas 3.1 界面绘制 3.2 范围裁切 3.3 集合变换 四 Path 4.1 添加图形 4 ...

  7. 【Android - 进阶】之自定义视图浅析

    1       概述 Android自定义View / ViewGroup的步骤大致如下: 1) 自定义属性: 2) 选择和设置构造方法: 3) 重写onMeasure()方法: 4) 重写onDra ...

  8. 【Android 应用开发】自定义View 和 ViewGroup

    一. 自定义View介绍 自定义View时, 继承View基类, 并实现其中的一些方法. (1) ~ (2) 方法与构造相关 (3) ~ (5) 方法与组件大小位置相关 (6) ~ (9) 方法与触摸 ...

  9. Android自定义View学习(四)

    硬件加速 参考:HenCoder Android 自定义 View 1-8 硬件加速 硬件加速能够让绘制变快,主要有三个原因: 本来由 CPU 自己来做的事,分摊给了 GPU 一部分,自然可以提高效率 ...

随机推荐

  1. 洛谷 P1635 跳跃

    题目: 题目背景 NOIP即将迎来周年华诞.在这一个春秋的历程里,NOIP领导全国oier,建设高效.稳定.快捷.开放的社会主义现代化OI.在新的一年里,YZOJ将再接再厉,积极探寻成长之路,更好地为 ...

  2. Flume框架的学习使用

    Flume框架的学习使用 Flume简介 Flume提供一个分布式的,可靠的,对大数据量的日志进行高效收集.聚集.移动的服务. Flume基于流失架构,容错性强,也很灵活简单 Flume,kafka用 ...

  3. 机器学习-FP Tree

    接着是上一篇的apriori算法: FP Tree数据结构 为了减少I/O次数,FP Tree算法引入了一些数据结构来临时存储数据.这个数据结构包括三部分,如下图所示 第一部分是一个项头表.里面记录了 ...

  4. hdu6396 Swordsman(贪心)

    Swordsman 题目传送门 解题思路 先将每种属性排序,因为打倒怪兽会使属性增强,所以肯定是能打就打,用cnt[i]记录怪兽i已经被超过的属性数量,如果被超过的属性数为k了,则打倒此怪兽,将获得的 ...

  5. IDEA中使用mybatis逆向工程

    如果使用过mybatis的人就会发现,当我们使用mybatis时,我们每次都需要自己手动创建实体类,映射文件(当然你也可以用注释),还有接口来进行使用,这样手动创建非常的繁琐,mybatis考虑到这方 ...

  6. 快速安装k8s,版本为1.13.8

    利用rpm快速部署k8s #!/bin/bash #快速安装k8s #by love19791126 107420988@qq.com pwd=$(pwd) masteripaddr=#(ip a s ...

  7. OI/ACM最全卡常大招

    NO.10: 循环展开: 在缓存和寄存器允许的情况下一条语句内大量的展开运算会刺激 CPU 并发(蛤?这是个什么原理,算了,反正写了没坏处就这么写吧) NO.9: 特殊运算优化:(或许这真的没用) 取 ...

  8. (转)Linux LVM逻辑卷配置过程详解(创建、扩展、缩减、删除、卸载、快照创建)

    一.预备知识 LVM全称为Logical Volume Manager 逻辑卷管理器,LVM是Linux环境中对磁盘分区进行管理的一种机制,是建立在硬盘和分区之上.文件系统之下的一个逻辑层,可提高磁盘 ...

  9. java并发编程(十一)----(JUC原子类)基本类型介绍

    上一节我们说到了基本原子类的简单介绍,这一节我们先来看一下基本类型: AtomicInteger, AtomicLong, AtomicBoolean.AtomicInteger和AtomicLong ...

  10. 算法与数据结构基础 - 合并查找(Union Find)

    Union Find算法基础 Union Find算法用于处理集合的合并和查询问题,其定义了两个用于并查集的操作: Find: 确定元素属于哪一个子集,或判断两个元素是否属于同一子集 Union: 将 ...