一直以来就很想学习自定义view,因为在做一些遥控类型的app时,有些组件用基本组件并不能完整的展示,自定义一个view就很重要。由于android开发学习还没有多久,自定义view就有点力不从心。刚好最近有些时间可以系统的学习一下,但是当我开始学习后发现和我想象还是有很大难度。

我的入门文章是《Carson带你学Android:手把手教你写一个完整的自定义View》

Carson带你学Android:手把手教你写一个完整的自定义View - 简书

非常有用,让我这样的门外汉知道了view的描画流程。这部分文章是一系列的文章,他把view的概念和相关的概念都讲解的很好,通俗易懂。

但是看过之后还是不能写出view,还需要真正的去动手写一个,在写的过程中进行查漏补缺,对号入座。

我在制作蓝牙遥控车app时有一个view组件,是一个模拟汽车档位的seekbar,如下图样子

档位旋钮只能在黑色线中进行移动,在抬手时旋钮需要就近到档位,并通知app当前档位。

我观察我的这个需求实际上像是一个不规则图形的seekbar。

于是我就开始在网上查找不规则图形的seekbar的例子代码,毕竟从0开始写即使学完了我还是没有这个能力。

我在网上找到了两个不规则图形的seekbar。一个是弧形seekbar。

《Android自定义View实现弧形SeekBar》

http://lastwarmth.win/2016/04/27/seekbar/

源码:GitHub - LiJia92/CustomArcSeekBar: 自定义弧形SeekBar

还有一个月牙形seekbar

《android:自定义SeekBar月牙形状》

android:自定义SeekBar月牙形状_Jarry的博客-CSDN博客

源码:GitHub - runfengai/MySeekbar: 月牙形状的SeekBar

我是先看的弧形seekbar,发现基本能看懂,于是就在这个的基础上进行的修改成我自己的seekbar。

下面我根据我自己的工程讲解一下

先上一张我自己的类图

View控件主要有3个类,

SeekBarArcView.java画背景黑线类

SeekBarBallView.java画档把小球类

GearSeekBarParent.java触摸事件的处理,并提供对外接口

画线

首先是画线,要在android中画线需要调用path类接口,主要有

public void lineTo(float x, float y)画直线

public void quadTo(float x1, float y1, float x2, float y2)画二次贝塞尔曲线

public void cubicTo(float x1, float y1, float x2, float y2,

float x3, float y3) 三次贝塞尔曲线

public void arcTo(RectF oval, float startAngle, float sweepAngle,

boolean forceMoveTo)画圆弧

基本就这些了,Carson带你学Android:自定义View Path类全面解析 - 简书

这篇文章讲的很不错,想详细了解的可以看一下。

如果想心形啊,五角星啊,只能通过这些接口进行组合实现,所以需要一些数学知识,特别是在画一些不规则图形时。

我的档位线就是几个直线进行组合,所以比较简单。

先定义一些点,这些点是线段的端点

这些端点定义如下图

// 此处设置0-8点坐标,这个坐标边距是固定的30,最好完善重比例
pointFt1.set(30, 30);
pointFt2.set(30, bottom - top - 30);
pointFt3.set((float) (right - left) / 2, 30);
pointFt4.set((float) (right - left) / 2, bottom - top - 30);
pointFt5.set(right - left - 30, 30);
pointFt6.set(right - left - 30, bottom - top - 30);
pointFt7.set(30, (float) (bottom - top) / 2);
pointFt8.set(right - left - 30, (float) (bottom - top) / 2);
pointFt0.set((float) (right - left) / 2, (float) (bottom - top) / 2);

然后在调用moveTo和lineTo进行画线

// 画档位移动线
path.moveTo(pointFt1.x, pointFt1.y);
path.lineTo(pointFt2.x, pointFt2.y);
path.moveTo(pointFt3.x, pointFt3.y);
path.lineTo(pointFt4.x, pointFt4.y);
path.moveTo(pointFt5.x, pointFt5.y);
path.lineTo(pointFt6.x, pointFt6.y);
path.moveTo(pointFt7.x, pointFt7.y);
path.lineTo(pointFt8.x, pointFt8.y);
canvas.drawPath(path, paint);

在protected void onDraw(Canvas canvas)中调用画线方法,onDraw会在绘制过程时被调用,具体流程在《Carson带你学Android:自定义View 绘制过程(Draw)》中有讲解。

画圆球

画圆球就比较简单了,计算出圆心坐标直接画圆即可。并让圆心在黑色线的轨迹中运动就行了。

canvas.drawCircle((float) getMeasuredWidth() / 2, (float) getMeasuredWidth() / 2, (float) getMeasuredWidth() / 2, paint);

具体在哪里画,就通过调用

ball.layout((int) (x - ball.getMeasuredWidth() / 2),(int) (y - ball.getMeasuredWidth() / 2),(int) (x + ball.getMeasuredWidth() / 2),(int) (y + ball.getMeasuredWidth() / 2));

进行描画。具体在什么地方能够描画是在public boolean onTouchEvent(MotionEvent event)的MotionEvent.ACTION_MOVE事件中进行的处理。为了在不同位置进行不同轨迹的移动,我把画图区域进行了划分,在不同的区域限制x或者y的坐标,这样就达到了在黑线内移动的要求。

在黄色区域x为固定值,为point1的x值,只有y变化,在蓝色区域x为固定值,为point3的x值,y可以变化,在白色区域x为point5的x值,y可以变化。这几个区域可以直接使用x坐标值作为判断。

在紫色和橘色菱形中需要y固定为point7的y值,x可以变化。如何判断在菱形中就需要使用到一些数学知识推到,也不难。我的大概推到如下

f1(x)和f2(x)用最基本的斜率为1的二元一次方程表达x+b=y,只需要带入其中一点坐标就可以得到b的值,(a1,b1)带入b=b1-a1

f3(x)和f4(x)用最基本的斜率为-1的二元一次方程表达-x+b=y,只需要带入其中一点坐标就可以得到b的值,(a1,b1)带入b=b1+a1

在菱形中需要满足f1(x)>y+a1-b1并且f2(x)<y+a2-b2并且f3(x)>a1+b1-y并且f4(x)<a2+b2-y

同时满足这四个条件就表明坐标点在菱形内。

以上图片代码实现如下

/*** 改变球的位置,将球在参数坐标处描画出来** @param currentX 当前x坐标* @param currentY 当前Y坐标*/
private void changeBallLayout(int currentX, int currentY) {float x = currentX;float y = currentY;if ((x > (y + pointFt7.x - pointFt7.y)) && (x < (y + pointFt0.x - pointFt0.y)) && (x > (pointFt7.x + pointFt7.y - y)) && (x < (pointFt0.x + pointFt0.y - y))) {y = pointFt7.y;} else if ((x > (y + pointFt0.x - pointFt0.y)) && (x < (y + pointFt8.x - pointFt8.y)) && (x > (pointFt0.x + pointFt0.y - y)) && (x < (pointFt8.x + pointFt8.y - y))) {y = pointFt0.y;} else if (x < ((pointFt1.x + pointFt3.x) / 2)) {x = pointFt1.x;} else if (x < ((pointFt3.x + pointFt5.x) / 2)) {x = pointFt3.x;} else {x = pointFt5.x;}if (y < pointFt1.y) {y = pointFt1.y;}if (y > pointFt2.y) {y = pointFt2.y;}circleCenter.set(x, y);ball.layout((int) (x - ball.getMeasuredWidth() / 2),(int) (y - ball.getMeasuredWidth() / 2),(int) (x + ball.getMeasuredWidth() / 2),(int) (y + ball.getMeasuredWidth() / 2));
}

到这里我们就能实现背景绘制,并且让小球在轨迹中运动。

我们还需要实现就近点吸附的效果,如果你的seekbar不需要这个功能,只想要像进度条那样的任意位置停靠,这个步骤就可以不做了。

我们需要判断小球坐标在哪个区域,这个区域属于哪个档位,在区域内进行档位设置。区域划分如下图。

在不同的区域返回不同的档位,区域横平竖直,所以只需要使用x,y坐标进行区分即可。

/*** 计算档次** @param event 事件,可以得到坐标* @return 档次*/
private int getLevel(MotionEvent event) {float x = event.getX();float y = event.getY();int result;if (x < ((pointFt1.x + pointFt3.x) / 2)) {if (y < pointFt7.y) {result = 1;} else {result = 2;}} else if (x < ((pointFt3.x + pointFt5.x) / 2)) {if (y < ((pointFt3.y + pointFt0.y) / 2)) {result = 3;} else if (y < ((pointFt0.y + pointFt4.y) / 2)) {result = 0;} else {result = 4;}} else {if (y < pointFt8.y) {result = 5;} else {result = 6;}}return result;
}

如果我们在手指抬起时根据最后档位跳到对应point坐标是可以的,但是就会出现小球闪现的现象。我们使用了一个过渡效果,public void startScroll(int startX, int startY, int dx, int dy, int duration)这个函数是Scroller类的滑动效果,从起始点使用设置时间滑动一定距离。

/*** 平滑滑动** @param startX 起始值X* @param startY 起始值Y* @param dx     X滑动距离* @param dy     Y滑动距离*/
public void smoothScrollLevel(int startX, int startY, int dx, int dy) {scroller.startScroll(startX, startY, dx, dy, 500);postInvalidate();
}

这个函数设置后只是进行滑动坐标点的计算,并不会去重新绘制小球,所以需要在计算完成回调函数中不停的绘制小球,达到滑动效果展示。

// 使用之前设置的startScroll计算出的坐标,返给该函数,该函数调用回调描画小球
@Override
public void computeScroll() {if (scroller.computeScrollOffset()) {if (listener != null) {listener.onSmoothScroll(scroller.getCurrX(), scroller.getCurrY());postInvalidate();}}
}

这里是一个回调,回调在初始化时注册了

ball.setListener(this::changeBallLayout);

就会不停地调用上面描画小球的方法。

然后是实现监听档位回调,需要两种,一种是在抬手时触发的最终档位回调,一种是在运动时触发的临时档位回调。

case MotionEvent.ACTION_MOVE:// 当手指移动时,使球跟手,不停地描画球的位置changeBallLayout((int) event.getX(), (int) event.getY());// 当手指移动时,计算经过的档位并更新经过档位currentLevel = getLevel(event);if (lastLevel != currentLevel) {lastLevel = currentLevel;if (tempListener != null) {tempListener.OnProgressTempChanged(currentLevel);}}break;
case MotionEvent.ACTION_UP:// 当手指离开View时,圆球平滑滑到最近的档次currentLevel = getLevel(event);int x;int y = (int) event.getY();if (currentLevel == 1) {x = (int) pointFt1.x;ball.smoothScrollLevel(x, y, 0, (int) (pointFt1.y - y));} else if (currentLevel == 2) {x = (int) pointFt1.x;ball.smoothScrollLevel(x, y, 0, (int) (pointFt2.y - y));} else if (currentLevel == 3) {x = (int) pointFt3.x;ball.smoothScrollLevel(x, y, 0, (int) (pointFt3.y - y));} else if (currentLevel == 0) {x = (int) pointFt3.x;ball.smoothScrollLevel(x, y, 0, (int) (pointFt0.y - y));} else if (currentLevel == 4) {x = (int) pointFt3.x;ball.smoothScrollLevel(x, y, 0, (int) (pointFt4.y - y));} else if (currentLevel == 5) {x = (int) pointFt5.x;ball.smoothScrollLevel(x, y, 0, (int) (pointFt5.y - y));} else if (currentLevel == 6) {x = (int) pointFt5.x;ball.smoothScrollLevel(x, y, 0, (int) (pointFt6.y - y));}if (listener != null) {listener.OnProgressChanged(currentLevel);}break;

至此,我们就把所有需要的完成了,只需要写一个demo测试一下。

xml如下

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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=".MainActivity"><androidx.appcompat.widget.LinearLayoutCompatandroid:layout_width="match_parent"android:layout_height="match_parent"android:gravity="center_horizontal"android:orientation="vertical"><com.example.myseekbar.GearSeekBarParentandroid:id="@+id/seek_bar"android:layout_width="600dp"android:layout_height="300dp"android:layout_marginTop="40dp"android:background="@color/purple_200"app:arcColor="#000000"app:arcWidth="3dp"app:ballColor="#00ffff"app:ballSize="30dp" /><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:gravity="center"android:text="有效档位"android:textSize="30sp" /><TextViewandroid:id="@+id/seek_bar_progress"android:layout_width="wrap_content"android:layout_height="wrap_content"android:gravity="center"android:text="0"android:textSize="30sp" /><TextViewandroid:layout_width="wrap_content"android:layout_height="wrap_content"android:gravity="center"android:text="临时档位"android:textSize="30sp" /><TextViewandroid:id="@+id/seek_bar_temp_progress"android:layout_width="wrap_content"android:layout_height="wrap_content"android:gravity="center"android:text="0"android:textSize="30sp" /></androidx.appcompat.widget.LinearLayoutCompat>
</FrameLayout>

MainActivity如下

package com.example.myseekbar;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;
import android.widget.TextView;public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);final TextView textView = findViewById(R.id.seek_bar_progress);final TextView textView1 = findViewById(R.id.seek_bar_temp_progress);GearSeekBarParent seekBar = findViewById(R.id.seek_bar);seekBar.setListener(level -> textView.setText(String.valueOf(level)));seekBar.setTempListener(level -> textView1.setText(String.valueOf(level)));}
}

效果如下

这里只讲述了一些算法和逻辑,具体流程需要学习上面的博文,一些代码细节还需要自己研究源码,源码如下

GearSeekBar: 自定义view学习,实现汽车档位的seekbar,对应博文https://blog.csdn.net/andylauren/article/details/122064101

希望通过这个代码的学习能够加深对自定义view的理解。

自定义形状seekbar学习相关推荐

  1. 举个栗子~Tableau 技巧(209):用自定义形状实现刻度环形图

    实际分析场景中,数据粉对美总是很执着,如何让分析占比的环形图,看着更漂亮点?比如给环形图增加刻度. 虽然数据分析还是应以实用为主,但既然 Tableau 是为美而生,那我们就试试看吧~ 这里,为大家分 ...

  2. 关于Echarts词云图自定义形状如何实现

    关于Echarts词云图如何实现自定义形状 文章目录 关于Echarts词云图如何实现自定义形状 前言 一.前期准备 二.形成步骤 1.转化图片为base64 2.填入代码,实现形状 前言 因为这段时 ...

  3. r语言各形状编号_R语言,超级英雄云词图,你们要的自定义形状来了

    原标题:R语言,超级英雄云词图,你们要的自定义形状来了 欢迎关注天善智能 hellobi.com,我们是专注于商业智能BI,大数据,数据分析领域的垂直社区,学习.问答.求职,一站式搞定! 天善学院sv ...

  4. python词云自定义形状_如何在Python中生成任何形状的词云

    作者 | Julia Kho 编辑| 代码医生团队 在本文中,我们将探讨如何在python中以您想要的任何形状生成文字云.我们将通过一个示例来说明如何在房屋的自定义形状中创建简单的文字云,如上图所示. ...

  5. draw.io创建自定义形状

    使用文本编辑器在diagrams.net中创建自定义形状 你可以在diagrams.net中创建自己的自定义模板(形状),通过XML格式描述模板中组件的几何形状.连接点和样式. 提示:你可以将自定义模 ...

  6. ps自定义形状工具_【福利】3000款PS自定义形状免费打包下载

    PS形状介绍 获取方式见文末 ps自定义形状素材包包含例如:品牌标志Logo.动物形状,花卉形状,圆圈形状,边框形状,人物形状,海洋生物形状,南瓜形状,万圣节形状,扑克牌形状等等.合计3000款!如果 ...

  7. “3D几何与视觉技术”全球在线研讨会第五期~隐式3D形状表示学习

    前几周跟大家分享了 3DGV 在线研讨会: "3D几何与视觉技术"全球在线研讨会(9月2日到12月16日) "3D几何与视觉技术"全球在线研讨会第二期 &quo ...

  8. power bi自定义地图_如何使用自定义形状图在Power BI中创建地理图

    power bi自定义地图 介绍 (Introduction) This is the third article of a series dedicated to discovering geogr ...

  9. java 自定义形状按钮_制作自定义背景Button按钮、自定义形状Button的全攻略

    在Android开发应用中,默认的Button是由系统渲染和管理大小的.而我们看到的成功的移动应用,都是有着酷炫的外观和使用体验的.因此,我们在开发产品的时候,需要对默认按钮进行美化.在本篇里,笔者结 ...

最新文章

  1. 北京低利用率数据中心将有序关闭腾退
  2. 计算机粘贴功能不能用了,电脑复制粘贴不能用了【解决办法】
  3. 互联网晚报 | 4月11日 星期一 | 苏州放宽住房限售;苹果确认开始在印度生产iPhone 13;民航局将上报民航专项检查方案...
  4. psv应用java_PSV内容管理功能详细说明
  5. Spring-beans-BeanPostProcessor/InitializingBean
  6. 计算机软驱的连接方式,岛精仿真软驱、斯托尔USB软驱、斯坦格电脑横机软盘改U盘...
  7. elementUI使用el-tabs时,页面崩溃卡死问题
  8. 安卓手机软件性能测试,手机性能评测软件
  9. php phalcon 中文手册,基础教程 · Phalcon 3.4中文手册 · 看云
  10. 33岁想从头学做网页设计_从头开始设计字体-并在24小时内将其提交给Google字体...
  11. 如何只用与非门、或非门构成或门、与门、非门?
  12. Fedformer中的小波变换(FEB-w模块)
  13. android 系统/本地日志打印
  14. maven依赖手动下载
  15. 可视化DIY制作小程序APP和网站时为什么能千变万化?
  16. Win10升级Win11必备的5款免费软件
  17. 太急了点吧?贴吧PWA20天就出炉了
  18. WordPress安装论坛插件
  19. 最棒的Visual Studio扩展
  20. python 模块 moviepy 视频剪辑

热门文章

  1. 计算机x线摄影的发展趋势,计算机X线摄影技术----CR 新进展
  2. python音乐可视化效果_python 音频可视化
  3. 恒讯科技讲解:量子云计算是什么?
  4. 如何写好一篇论文——闵老师《论文写作》心得
  5. 手机处理器排行榜2019_2019十大手机读书软件排行榜
  6. Opencv 笔记8 霍夫变换
  7. “分众1000万美元收购网络打手论坛”—事件营销乎?
  8. 【AI系列】1关于人工智能发展历史、资料推荐、技术体系的整理
  9. JAVA中RandomAccess接口
  10. vue webapp之music(六)利用axios与后端接口代理请求歌单推荐数据