热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

自定义View1——BezierBottomBar

这个是在项目中运用的自定义View的第一篇,按照字母序,第一篇首先先讲BezierBottomBar。这个控件是我从郭霖公众号之前的一篇推送上学习来的,所以有些代码是照搬的之前那篇

这个是在项目中运用的自定义View的第一篇,按照字母序,第一篇首先先讲BezierBottomBar。这个控件是我从郭霖公众号之前的一篇推送上学习来的,所以有些代码是照搬的之前那篇推送,这篇文章也有很多地方直接引用了这篇推送的一些内容,下面是那篇的推送地址

https://mp.weixin.qq.com/s?__biz=MzA5MzI3NjE2MA==&mid=2650243121&idx=1&sn=a3e3368758074d509691e531a927f2c8&chksm=8863715ebf14f848f4c648575cba1d26313ad88893fb3eaf3169aa7d10c44476b779a3438409&mpshare=1&scene=23&srcid=08207tLPp42M8JTyyGQcQrJm#rd

先看效果

《自定义View1——BezierBottomBar》

这个是结合了ViewPager后的效果。
这个控件总共分为三个部分,可以把这部分看做是一个VVM模式

  • 控件本体——BezierBottomBarView
  • 控制View行为的——BezierBottomBarControl
  • 定制的ViewPager

BezierBottomBarView

测量View大小

由于我们是做的是一个底边栏,所以我们要在onMeasure中设定最大高度,以防止控件占高度太高。所以在getHeight方法中,将控件最大高度设置为屏幕的1/8.

private int getHeight(int heightMeasureSpec) {
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
DisplayMetrics dm = getResources().getDisplayMetrics();
int height = (heightSize > dm.heightPixels) ? heightSize : dm.heightPixels;
return (height / 8);
}

对于单个圆的存放

在那篇推送中作者是将这个控件写成了ViewGroup+ImgView的形式,但是在我们的应用中我们想要加入手势控制动画,所以如果仍然使用作者的思路就会导致动画实现起来相对来说比较的麻烦,故而我改用纯View进行绘制。
所以我们就需要一类数据来表示单个圆的各种参数,在这里我将get和set方法省略,如果有需要可以去查看源码。

public static class BarTag {
private int icon;
private String tag;
private int color;
private float centerX;
private float centerY;
private float radius;
private RectF rectF;
private RectF dst;
private RectF tagRectF;
}

在这里预留了tag和图标的color方法,在里面并没有使用,如果未来有需要的话可以自行添加。这个类中主要持有的方法就是icon,radius和dst,dst表示圆的位置。

确定圆的摆放位置

在原文中,确定摆放位置是在onLayout中设置的,但是原文是将控件当做一个ViewGroup去写的,我在这里却是将其当做View去写,所以我选择在onSizeChanged中去确定圆的位置,并且存储起来,在之后的onDraw方法后用canvas去绘制外面的圆形。

在这里有一点补充:
继承与View和继承与现有控件都是下面的顺序,但是控件的大小是生成之后就固定的,不会再次改变。
onMeasure()→onSizeChanged()→onLayout()→onMeasure()→onLayout()→onDraw()

所以在onSizeChanged中我们可以这么确定一个圆的位置,interval表示两个圆之间的间隔。

interval = (width - 2 * tabNum * radius) / (tabNum + 1);
for (int i = 0; i float cx = interval + radius + i * (interval + 2 * radius);
RectF rectF = new RectF(cx - radius, startY - radius, cx + radius, startY + radius);
barTags.get(i).setRectF(rectF);
RectF dst = new RectF((int) (interval + (1 - scale * 1 / g2) * radius + i * (interval + 2 * radius)),
(int) (startY - scale * radius / g2),
(int) (interval + (1 + scale * 1 / g2) * radius + i * (interval + 2 * radius)),
(int) (startY + scale * radius / g2));
barTags.get(i).setDst(dst);
barTags.get(i).setRadius(radius);
}

在onSizeChanged方法中计算并且将圆的位置进行存储

对静止圆的绘制

在计算完各个圆的位置之后,我们就可以在onDraw方法中进行绘制。可以看到,如果当前圆被选中,那么就会有一个颜色填充。
所以大概就是这样

for (int i = 0; i float cx = barTags.get(i).centerX;
float cy = barTags.get(i).centerY;
canvas.drawCircle(cx, cy, radius, linePaint);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), barTags.get(i).icon);
canvas.drawBitmap(bitmap, null, barTags.get(i).dst, fillPaint);
}

动画的绘制

(有关贝塞尔曲线及其内容请直接参考开始给出的博客,我觉得讲的不错)
我们可以仔细 的观察到,在这个动画当中,中间移动的圆分成了6个状态
这里是起始的三个状态,分别是圆,开始向左移动,向左移动一定距离,还欠缺的是准备结束向左移动,向左移动的过量和向左移动的回弹

《自定义View1——BezierBottomBar》 这里只能说是三个状态

那么我们可以假设动画时间currentTime(0~1),所以我们可以将整个移动状态分为以下几个区间段:

  • 状态1,圆
    currentTime = 0
  • 状态2,即向左移动
    0
  • 状态3,即开始进入中间状态
    0.2
  • 状态4,即准备结束这段运动,是状态2的镜面对称
    0.5
  • 状态5,结束这段运动时的回弹开始
    0.8
  • 状态6,结束这段运动时候的回弹结束
    0.9
  • 状态1,圆
    currentTime = 1

由之前那篇博客我们可以知道,使用二阶贝塞尔曲线去画一个圆,受制于p1,p2,p3,p4四个点,所以我们就可以通过改变这四个点的参数去改变这个圆的参数

《自定义View1——BezierBottomBar》 这张图是原博客中的一张图,我就直接拿来用了

那么我们可以将这六个部分的代码变成这个样子

  • 状态1

if (currentTime == 0) {
resetP();
canvas.drawCircle(interval + radius + (currentPos) * (interval + 2 * radius), startY, 0, clickPaint);
fillPaint.setColor(startColor);
canvas.translate(startX, startY);
if (toPos > currentPos) {
p2.setX(radius);
} else {
p4.setX(-radius);
}
}

  • 状态2

if (currentTime > 0 && currentTime <= 0.2) {
direction = toPos > currentPos ? true : false;
if (animating) {
canvas.drawCircle(interval + radius + (toPos) * (interval + 2 * radius),
startY,
radius * 1.0f * 5 * currentTime,
clickPaint);
}
canvas.translate(startX, startY);
if (toPos > currentPos) {
p2.setX(radius + 2 * 5 * currentTime * radius / 2);
} else {
p4.setX(-radius - 2 * 5 * currentTime * radius / 2);
}
}

  • 状态3

if (currentTime > 0.2 && currentTime <= 0.5) {
float cx = startX + (currentTime - 0.2f) * distance / 0.7f;
canvas.translate(cx, startY);
if (toPos > currentPos) {
p1.setX(0.5f * radius * (currentTime - 0.2f) / 0.3f);
p2.setX(2 * radius);
p3.setX(0.5f * radius * (currentTime - 0.2f) / 0.3f);
p2.setMc(mc + (currentTime - 0.2f) * mc / 4 / 0.3f);
p4.setMc(mc + (currentTime - 0.2f) * mc / 4 / 0.3f);
} else {
p1.setX(-0.5f * radius * (currentTime - 0.2f) / 0.3f);
p3.setX(-0.5f * radius * (currentTime - 0.2f) / 0.3f);
p4.setX(-2 * radius);
p2.setMc(mc + (currentTime - 0.2f) * mc / 4 / 0.3f);
p4.setMc(mc + (currentTime - 0.2f) * mc / 4 / 0.3f);
}
}

  • 状态4

if (currentTime > 0.5 && currentTime <= 0.8) {
float cx = startX + (currentTime - 0.2f) * distance / 0.7f;
canvas.translate(cx, startY);
if (toPos > currentPos) {
p1.setX(0.5f * radius + 0.5f * radius * (currentTime - 0.5f) / 0.3f);
p3.setX(0.5f * radius + 0.5f * radius * (currentTime - 0.5f) / 0.3f);
p2.setMc(1.25f * mc - 0.25f * mc * (currentTime - 0.5f) / 0.3f);
p4.setMc(1.25f * mc - 0.25f * mc * (currentTime - 0.5f) / 0.3f);
} else {
p1.setX(-0.5f * radius - 0.5f * radius * (currentTime - 0.5f) / 0.3f);
p3.setX(-0.5f * radius - 0.5f * radius * (currentTime - 0.5f) / 0.3f);
p2.setMc(1.25f * mc - 0.25f * mc * (currentTime - 0.5f) / 0.3f);
p4.setMc(1.25f * mc - 0.25f * mc * (currentTime - 0.5f) / 0.3f);
}
}

  • 状态5

if (currentTime > 0.8 && currentTime <= 0.9) {
p2.setMc(mc);
p4.setMc(mc);
float cx = startX + (currentTime - 0.2f) * distance / 0.7f;
canvas.translate(cx, startY);
if (toPos > currentPos) {
p4.setX(-radius + 1.6f * radius * (currentTime - 0.8f) / 0.1f);
} else {
p2.setX(radius - 1.6f * radius * (currentTime - 0.8f) / 0.1f);
}
}

  • 状态6

if (currentTime > 0.9 && currentTime <1) {
if (toPos > currentPos) {
p1.setX(radius);
p3.setX(radius);
canvas.translate(startX + distance, startY);
p4.setX(0.6f * radius - 0.6f * radius * (currentTime - 0.9f) / 0.1f);
} else {
p1.setX(-radius);
p3.setX(-radius);
canvas.translate(startX + distance, startY);
p2.setX(-0.6f * radius + 0.6f * radius * (currentTime - 0.9f) / 0.1f);
}
}

View的hide和show

show

可以看到,这个show的动画有一个稍微过一些然后再回弹的效果(虽然可能真的不太明显)并且圆是依次上升的,所以就需要让它有一个上升的次序。

《自定义View1——BezierBottomBar》

我们可以看到,在sin图像中,在顶点下任取一个y值,都会有两个x值使得sin(x) = y(→_→好像讲的有点啰嗦)那么我们亦可以通过给不同的圆设置不同的初始值,来实现阶梯式上升的效果。

void show() {
if (!valueRunning && !hideRunning) {
float cy = (startY + radius) / (float) Math.sin(Math.toRadians(angle));
for (int i = 0; i hideHeight = cy + startY / (float) Math.sin(Math.toRadians(angle));
angles[i] = i * (-10);
dsts[i] = new RectF((int) (interval + (1 - scale * 1 / g2) * radius + i * (interval + 2 * radius)),
(int) (startY - scale * radius / g2) + cy,
(int) (interval + (1 + scale * 1 / g2) * radius + i * (interval + 2 * radius)),
(int) (startY + scale * radius / g2) + cy);
}
animState = AnimState.Show;
handler.postDelayed(showRunnable, showTime);
}
}

hide

hide方法和show方法相同,也需要几个圆依次向下。所以思路也同show——赋予几个圆不同的负向初始量,然后在handle中对其进行改变

void hide() {
if (!valueRunning && !showRunning) {
hideSpeed = (startY + radius + (tabNum - 1) * (radius * 2 / tabNum)) / hideTime;
for (int i = 0; i changeHeight[i] = i * (-10);
dsts[i] = new RectF((int) (interval + (1 - scale * 1 / g2) * radius + i * (interval + 2 * radius)),
(int) (startY - scale * radius / g2),
(int) (interval + (1 + scale * 1 / g2) * radius + i * (interval + 2 * radius)),
(int) (startY + scale * radius / g2));
}
animState = AnimState.Hide;
handler.postDelayed(hideRunnable, hideTime);
}
}

控件的单击事件

在这个控件中,我们有两个手势操作,以及单击操作。但是手势操作我们需要在整个屏幕,也就是在Activity中去操作这个全局手势,所以我们就只需要在控件的onTouchEvent中处理单击操作即可。
由于控件有show和hide两种状态,所以我们只需要让其在show的时候处理Event即可,在hide的时候我们可以选择无视。
所以在这部分就可以这么去处理TouchEvent

if (x > interval + 2 * radius && x <(interval + 2 * radius) * tabNum) {
if (animator != null) {
animator.cancel();
}
int toPos = (int) (x / (interval + 2 * radius));
if (toPos != currentPos && toPos <= tabNum) {
startAniTo(currentPos, toPos);
}
} else if (x > interval && x if (animator != null) {
animator.cancel();
}
if (currentPos != 0)
startAniTo(currentPos, 0);
}

我们可以看到,在这里就只需要判断单击的x,y值即可。

BezierBottomBarControl

由于需要在全局设置一个手势操作,并且需要在一定时间过后对BottomBar进行一个隐藏,所以需要一个ViewControl来统一的对View的状态进行操作。

设置手势操作

由于这个控件在App中仅存在于MainActivity中,所以只需要在MainActivity的onTouchEvent中将MotionEvent传入control就可以了。在Control中对其进行处理。由于我们并不需要实时的使得控件对于TouchEvent进行反馈,所以我们只需获取到Down和Up的坐标即可。

public void setTouchEvent(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
if (lastX == 0) {
lastX = viewPager.getLastX();
}
if (lastY == 0) {
lastY = viewPager.getLastY();
}
float deltaY = lastY - y;
if (deltaY > 0) {
if (bottomBar.getState() == BezierBottomBarView.AnimState.Hide) {
show();
}
}
if (deltaY <0) {
if (bottomBar.getState() != BezierBottomBarView.AnimState.Hide) {
hide();
}
}
break;
}
}

show和hide

在control中起了一个定时器,当控件显示一段时间过后,就会自动调用hide方法,使得控件进行隐藏。

setViewPagerListener

由于我们的控件可以和ViewPager进行一个联动,在Control的构造方法中已经将ViewPager的实例传入,所以我们只需要设置ViewPager的Listener即可。


推荐阅读
  • 本文深入探讨了Android事件分发机制的源代码,重点分析了DecorView作为Activity根布局的角色及其在事件传递中的作用。同时,详细解析了PhoneWindow在Activity窗口管理中的关键功能,以及它如何与DecorView协同工作,确保用户交互事件的高效处理。 ... [详细]
  • Android 图像色彩处理技术详解
    本文详细探讨了 Android 平台上的图像色彩处理技术,重点介绍了如何通过模仿美图秀秀的交互方式,利用 SeekBar 实现对图片颜色的精细调整。文章展示了具体的布局设计和代码实现,帮助开发者更好地理解和应用图像处理技术。 ... [详细]
  • 本文深入探讨了 MXOTDLL.dll 在 C# 环境中的应用与优化策略。针对近期公司从某生物技术供应商采购的指纹识别设备,该设备提供的 DLL 文件是用 C 语言编写的。为了更好地集成到现有的 C# 系统中,我们对原生的 C 语言 DLL 进行了封装,并利用 C# 的互操作性功能实现了高效调用。此外,文章还详细分析了在实际应用中可能遇到的性能瓶颈,并提出了一系列优化措施,以确保系统的稳定性和高效运行。 ... [详细]
  • 表面缺陷检测数据集综述及GitHub开源项目推荐
    本文综述了表面缺陷检测领域的数据集,并推荐了多个GitHub上的开源项目。通过对现有文献和数据集的系统整理,为研究人员提供了全面的资源参考,有助于推动该领域的发展和技术进步。 ... [详细]
  • 为了优化直播应用底部聊天框的弹出机制,确保在不同设备上的布局稳定性和兼容性,特别是在配备虚拟按键的设备上,我们对用户交互流程进行了调整。首次打开应用时,需先点击首个输入框以准确获取键盘高度,避免直接点击第二个输入框导致的整体布局挤压问题。此优化通过调整 `activity_main.xml` 布局文件实现,确保了更好的用户体验和界面适配。 ... [详细]
  • POJ 1696: 空间蚂蚁算法优化与分析
    针对 POJ 1696 的空间蚂蚁算法进行了深入的优化与分析。本研究通过改进算法的时间复杂度和空间复杂度,显著提升了算法的效率。实验结果表明,优化后的算法在处理大规模数据时表现优异,能够有效减少计算时间和内存消耗。此外,我们还对算法的收敛性和稳定性进行了详细探讨,为实际应用提供了可靠的理论支持。 ... [详细]
  • Android ListView 自定义 CheckBox 实现列表项多选功能详解
    本文详细介绍了在Android开发中如何在ListView的每一行添加CheckBox,以实现列表项的多选功能。用户不仅可以通过点击复选框来选择项目,还可以通过点击列表的任意一行来完成选中操作,提升了用户体验和操作便捷性。同时,文章还探讨了相关的事件处理机制和布局优化技巧,帮助开发者更好地实现这一功能。 ... [详细]
  • 深入解析十大经典排序算法:动画演示、原理分析与代码实现
    本文深入探讨了十种经典的排序算法,不仅通过动画直观展示了每种算法的运行过程,还详细解析了其背后的原理与机制,并提供了相应的代码实现,帮助读者全面理解和掌握这些算法的核心要点。 ... [详细]
  • 本文探讨了协同过滤算法在推荐系统中的应用,重点介绍了基于用户和基于物品的两种协同过滤方法。通过引入相似度评估技术和交替最小二乘优化技术,显著提升了推荐系统的准确性和鲁棒性。实验结果表明,该方法在处理大规模数据集时表现出色,能够有效提高用户满意度和系统性能。 ... [详细]
  • 如何在Android应用中设计和实现专业的启动欢迎界面(Splash Screen)
    在Android应用开发中,设计与实现一个专业的启动欢迎界面(Splash Screen)至关重要。尽管Android设计指南对使用Splash Screen的态度存在争议,但一个精心设计的启动界面不仅能提升用户体验,还能增强品牌识别度。本文将探讨如何在遵循最佳实践的同时,通过技术手段实现既美观又高效的启动欢迎界面,包括加载动画、过渡效果以及性能优化等方面。 ... [详细]
  • 掌握DSP必备的56个核心问题,我已经将其收藏以备不时之需! ... [详细]
  • 本文探讨了将PEBuilder转换为DIBooter.sh的方法,重点介绍了如何将DI工具集成到启动层,实现离线镜像引导安装。通过使用DD命令替代传统的grub-install工具,实现了GRUB的离线安装。此外,还详细解析了bootice工具的工作原理及其在该过程中的应用,确保系统在无网络环境下也能顺利引导和安装。 ... [详细]
  • 本文深入探讨了Java枚举类型的使用与实践,详细解析了枚举的基本用法及其在实际开发中的应用。首先介绍了枚举作为常量的替代方案,自JDK 1.5起,通过枚举可以更加简洁、安全地定义常量,避免了传统方式中可能出现的错误。此外,文章还探讨了枚举在实现单例模式、状态机等场景中的优势,并提供了多个实际案例,帮助开发者更好地理解和运用这一强大的语言特性。 ... [详细]
  • 题目描述:21世纪水果公司专注于开发新型水果品种。本研究通过高级水果的最长公共子序列路径分析,探讨了不同水果品种之间的遗传关系和进化路径,为新品种的培育提供了科学依据。该方法不仅提高了品种鉴定的准确性,还为遗传多样性研究提供了新的视角。 ... [详细]
  • 在上篇文章的基础上,本文将继续探讨 Linux 设备驱动中的设备模型与 `devicedriverbus` 机制。在将设备注册到总线之前,需要先创建 `device` 对象。可以通过静态定义 `device` 结构体变量,并调用 `device_register` 函数来完成这一过程。此外,文章还将详细解析设备模型的内部工作机制,以及 `devicedriverbus` 机制如何实现设备与驱动的自动匹配和管理。 ... [详细]
author-avatar
手机用户2502936971
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有