Android自定义View实战(SlideTab-可滑动的选择器)
转载请标明出处:
http://blog.csdn.net/xmxkf/article/details/52178553
本文出自:【openXu的博客】
目录:
这篇博客我们来一发自定义控件的实战,恰好前些天有一个小需求,效果图如下:
根据效果图,我们可以确定,用自定义View完全可以搞定,在自定义控件系列博客第一篇中,我们总结了自定义View的几个步骤:
- 继承View,覆盖构造方法
- 自定义属性
- 重写onMeasure方法测量宽高
- 重写onDraw方法绘制控件
当然,你没有必要完全依照步骤去做,这个步骤是你对控件应该怎么写已经有了完整的思路和规划,这在实际情况下是不现实的,往往我们自定义控件都是做到哪里缺什么就做什么,首先我们应该将它画出来,有一个可视的供我们思考的视图。所以,这里我们将这个步骤灵活的变换一下,由于我们现在还不确定需要自定义哪些属性,以及需要怎样测量,所以我们把这两个步骤挪到后面。
1. 初步分析,重写onDraw绘制
首先我们分析一下这个控件里面有哪些元素,有一条直线,上面有n个选项,分布着n个圆,当选中哪一个后这上面的圆变为蓝色的,还有n项字,当选中后字变为蓝色。下面我们初步确定一下需要的常量和一些简单的计算:
- 一个供选择的数组
String[] tabNames = new String[]{"tab1","tab2","tab3","tab4"} - 一些必要的数据:字体大小
mTextSize,字体颜色mColorTextDef,线段和圆圈的颜色mColorDef,被选中后的颜色mColorSelected,直线的高度mLineHight,圆圈的直径mCircleHight,被选中后蓝色空心圆圈的宽度mCircleSelStroke,当前选中的序号selectedIndex - 直线的长度
float lineLength=整个控件的宽度-左边圆圈的半径 -右边圆圈的半径(为了让直线两端正好在两端圆圈的中心) - 圆圈的分布间隔距离
float splitLength = lineLength / (n-1); - 字体与上面部分的间距
mMarginTop
在动手之前,我们要注意:直线的长度应该在控件完成测量后才能计算,所以应该在onMeasure中计算。现在我们可以动手了,首先继承View,覆盖构造方法,然后重写onDraw,在上面画出初步的轮廓。
代码:
public class SlideTab extends View {
String TAG = "SlidingTab";
private int mTextSize; //文本的字体大小
private int mColorTextDef; // 默认文本的颜色
private int mColorDef; // 线段和圆圈颜色
private int mColorSelected; //选中的字体和圆圈颜色
private int mLineHight; //基准线高度
private int mCircleHight; //圆圈的高度(直径)
private int mCircleSelStroke; //被选中圆圈(空心)的粗细
private int mMarginTop; //圆圈和文字之间的距离
private String[] tabNames; //需要绘制的文字
/**
* 下面需要计算
*/
private float splitLengh; //每一段横线长度
private int textStartY; //文本绘制的Y轴坐标
private List<Rect> mBounds; //保存文本的量的结果
private int selectedIndex = 0; //当前选中序号
private Paint mTextPaint; //绘制文字的画笔
private Paint mLinePaint; //绘制基准线的画笔
private Paint mCirclePaint; //绘制基准线上灰色圆圈的画笔
private Paint mCircleSelPaint; //绘制被选中位置的蓝色圆圈的画笔
public SlideTab(Context context) {
this(context, null);
}
public SlideTab(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SlideTab(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//初始化属性
tabNames = new String[]{"tab1","tab2","tab3","tab4"};
mColorTextDef = Color.GRAY;
mColorSelected = Color.BLUE;
mColorDef = Color.argb(255,234,234,234); //#EAEAEA
mTextSize = 20;
mLineHight = 5;
mCircleHight = 20;
mCircleSelStroke = 10;
mMarginTop = 50;
mLinePaint = new Paint();
mCirclePaint = new Paint();
mTextPaint = new Paint();
mCircleSelPaint = new Paint();
mLinePaint.setColor(mColorDef);
mLinePaint.setStyle(Paint.Style.FILL);//设置填充
mLinePaint.setStrokeWidth(mLineHight);//笔宽像素
mLinePaint.setAntiAlias(true);//锯齿不显示
mCirclePaint.setColor(mColorDef);
mCirclePaint.setStyle(Paint.Style.FILL);//设置填充
mCirclePaint.setStrokeWidth(1);//笔宽像素
mCirclePaint.setAntiAlias(true);//锯齿不显示
mCircleSelPaint.setColor(mColorSelected);
mCircleSelPaint.setStyle(Paint.Style.STROKE); //空心圆圈
mCircleSelPaint.setStrokeWidth(mCircleSelStroke);
mCircleSelPaint.setAntiAlias(true);
mTextPaint.setTextSize(mTextSize);
mTextPaint.setColor(mColorTextDef);
mLinePaint.setAntiAlias(true);
measureText();
}
/**
* measure the text bounds by paint
*/
private void measureText(){
mBounds = new ArrayList<>();
for(String name : tabNames){
Rect mBound = new Rect();
mTextPaint.getTextBounds(name, 0, name.length(), mBound);
mBounds.add(mBound);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
initConstant();
}
private void initConstant(){
int lineLengh = getWidth() - getPaddingLeft() - getPaddingRight() - mCircleHight;
splitLengh = lineLengh/(tabNames.length-1);
textStartY = mCircleHight + mMarginTop + getPaddingTop();
}
@Override
protected void onDraw(Canvas canvas) {
//画灰色基准线
canvas.drawLine(mCircleHight/2, mCircleHight/2, getWidth()-mCircleHight/2,mCircleHight/2 , mLinePaint);
float centerY = mCircleHight/2;
for(int i = 0; i<tabNames.length; i++){
float centerX = mCircleHight/2+(i*splitLengh);
//float cx, float cy, float radius, @NonNull Paint paint
//画基准线上灰色小圆圈
// Log.v(TAG, "画圆:X:"+centerX+" Y:"+centerY);
canvas.drawCircle(centerX, centerY,mCircleHight/2,mCirclePaint);
mTextPaint.setColor(mColorTextDef);
if(selectedIndex == i){
//画选中位置的蓝色圆圈
mCircleSelPaint.setStrokeWidth(mCircleSelStroke);
mCircleSelPaint.setStyle(Paint.Style.STROKE);
// Log.v(TAG, "画圆:X:"+centerX+" Y:"+centerY+" 半径:"+(mCircleHight-mCircleSelHight)/2);
canvas.drawCircle(centerX, centerY, (mCircleHight-mCircleSelStroke)/2, mCircleSelPaint);
mTextPaint.setColor(mColorSelected);
}
//绘制文字
float startX;
if(i == 0){
startX = 0;
}else if(i == tabNames.length-1){
startX = getWidth()-mBounds.get(i).width();
}else{
startX = centerX-(mBounds.get(i).width()/2);
}
// Log.v(TAG, "写字:X:"+startX+" Y:"+textStartY +" 字宽度:"+mBounds.get(i).width());
canvas.drawText(tabNames[i], startX, textStartY, mTextPaint);
}
}
}
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dip">
<com.openxu.st.SlideTab
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#aaff0000"/>
</LinearLayout>
运行效果:
2. 重写onMeasure计算宽高
基本的效果图已经出来了,不知道你们有没有发现,我在写布局文件的时候设置的高度是wrap_content,并且为控件设置了红色背景以便于参考,运行结果显示控件的高度却占满的整个屏幕,所以我们应该用重写onMeasure测量控件的高度(不熟悉onMeasure可以参照博客Android自定义View(三、深入解析控件测量onMeasure))。对于此控件,它的高度设置为填充父窗体,高度应该是圆圈的直径+字体的高度+字体与上面部分的距离。
重写onMeasure:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec); //获取宽的模式
int heightMode = MeasureSpec.getMode(heightMeasureSpec); //获取高的模式
int widthSize = MeasureSpec.getSize(widthMeasureSpec); //获取宽的尺寸
int heightSize = MeasureSpec.getSize(heightMeasureSpec); //获取高的尺寸
int height ;
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
float textHeight = mBounds.get(0).height();
height = (int) (textHeight + mCircleHight + mMarginTop);
// Log.v(TAG, "文本的高度:"+textHeight + "控件的高度:"+height);
}
//保存测量宽度和测量高度
setMeasuredDimension(widthSize, height);
initConstant();
}
运行结果:
发现高度还是不对,其实这个地方并不是上面重写onMeasure有问题,而是绘制文本的Y坐标的问题,我们看看drawText方法的注释:
/**
* Draw the text, with origin at (x,y), using the specified paint. The
* origin is interpreted based on the Align setting in the paint.
*
* @param text The text to be drawn
* @param x The x-coordinate of the origin of the text being drawn
* @param y The y-coordinate of the baseline of the text being drawn
* @param paint The paint used for the text (e.g. color, size, style)
*/
public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint) {
native_drawText(mNativeCanvasWrapper, text, 0, text.length(), x, y, paint.mBidiFlags,
paint.getNativeInstance(), paint.mNativeTypeface);
}
对于参数y的说明中,它指的是baseline的y轴坐标,而不是文字top的y坐标,对于baseline,后面再做说明,所以,我们计算textStartY的时候,应该计算baseline的y坐标:
private void initConstant(){
int lineLengh = getWidth() - getPaddingLeft() - getPaddingRight() - mCircleHight;
splitLengh = lineLengh/(tabNames.length-1);
// FontMetrics对象
Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
textStartY = getHeight() - (int)fontMetrics.bottom; //baseLine的位置
// textStartY = mCircleHight + mMarginTop + getPaddingTop();
}
再看看运行效果:
3. 重写onTouch加入滑动效果
现在,文字显示已经没有问题了,接下来,我们加入手指滑动的效果。此控件只支持左右滑动,手指滑动到某个位置的时候记录xy的坐标值,然后将蓝色选中的圆圈移动到x位置,其实就是在手指的位置画一个蓝色的圆圈,还要根据x的值计算当前偏向于选择哪一个标签。这里需要注意的地方是event.getX()和event.getY()获取到的手指的坐标是相对于本控件左上角的坐标(本控件左上角为原点),具体看下面代码,注释已经很清楚了:
@Override
protected void onDraw(Canvas canvas) {
//画灰色基准线
canvas.drawLine(mCircleHight/2, mCircleHight/2, getWidth()-mCircleHight/2,mCircleHight/2 , mLinePaint);
float centerY = mCircleHight/2;
for(int i = 0; i<tabNames.length; i++){
float centerX = mCircleHight/2+(i*splitLengh);
//float cx, float cy, float radius, @NonNull Paint paint
//画基准线上灰色小圆圈
// Log.v(TAG, "画圆:X:"+centerX+" Y:"+centerY);
canvas.drawCircle(centerX, centerY,mCircleHight/2,mCirclePaint);
mTextPaint.setColor(mColorTextDef);
if(selectedIndex == i){
if(!isSliding){
//画选中位置的蓝色圆圈
mCircleSelPaint.setStrokeWidth(mCircleSelStroke);
mCircleSelPaint.setStyle(Paint.Style.STROKE);
// Log.v(TAG, "画圆:X:"+centerX+" Y:"+centerY+" 半径:"+(mCircleHight-mCircleSelHight)/2);
canvas.drawCircle(centerX, centerY, (mCircleHight-mCircleSelStroke)/2, mCircleSelPaint);
}
mTextPaint.setColor(mColorSelected);
}
//绘制文字
float startX;
if(i == 0){
startX = 0;
}else if(i == tabNames.length-1){
startX = getWidth()-mBounds.get(i).width();
}else{
startX = centerX-(mBounds.get(i).width()/2);
}
// Log.v(TAG, "写字:X:"+startX+" Y:"+textStartY +" 字宽度:"+mBounds.get(i).width());
canvas.drawText(tabNames[i], startX, textStartY, mTextPaint);
}
//画手指拖动位置圆圈,最后画,避免被其他圆圈覆盖
if(isSliding){
// Log.v(TAG, "手指拖动画圆:X:"+slidX+" Y:"+centerY+" 半径:"+mCircleHight/2);
mCircleSelPaint.setStrokeWidth(1);
mCircleSelPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(slidX, centerY, mCircleHight/2, mCircleSelPaint);
}
}
private boolean isSliding = false; //手指是否在拖动
private float slidX, slidY; //手指当前位置(相对于本控件左上角的坐标)
@Override
public boolean onTouchEvent(MotionEvent event) {
slidX = event.getX(); //以本控件左上角为坐标原点
slidY = event.getY();
//左右越界
if(slidX< mCircleHight/2)
slidX = mCircleHight/2;
if(slidX>(getWidth() - mCircleHight/2))
slidX = getWidth() - mCircleHight/2;
Log.e(TAG, "手指位置: getX:"+slidX+" getY:"+slidY);
float select = slidX/splitLengh;
int xs = (int)(select*10)-(((int)select)*10);
selectedIndex = (int)select +(xs>5?1:0);
// Log.w(TAG, "手指位置在第"+select+"位置,小数为:"+xs+" ,选中的序列为:"+selectedIndex);
//TODO 如果要求手指脱离了直线所在矩形之后停止滑动,放开下面代码
/* if(slidY>mCircleHight || slidY < 0){
Log.e(TAG, "手指落在外面了");
if(isSliding){ //滑动到外面的,这时候需要重新绘制一次,其他事件不用重绘
isSliding = false;
invalidate();
}
isSliding = false;
return super.onTouchEvent(event);
}*/
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
isSliding = true;
// Log.e(TAG, "手指按下: getX:"+slidX+" getY:"+slidY);
break;
case MotionEvent.ACTION_MOVE:
// Log.i(TAG, "手指滑动: getX:"+slidX+" getY:"+slidY);
break;
case MotionEvent.ACTION_UP:
// Log.e(TAG, "手指抬起: getX:"+slidX+" getY:"+slidY);
isSliding = false;
break;
}
invalidate();
return true;
}
效果图:
4. 自定义属性
目前为止,控件基本能够正常使用了,如果你认为这样就可以了,那就不用往下看了。这个样子使用起来很不方便,如果很多地方需要用到此控件,而且控件中的字体大小颜色等都不一样,那是不是得写很多这样的控件(只是改变一下里面一些常量的值)?所以为了让这个控件使用更加灵活,可以自定义一些属性,这样只需要在布局文件中设置属性值即可。自定义属性具体方法请参见(Android自定义View(二、深入解析自定义属性))。
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--
private int mColorTextDef; // 默认文本的颜色
private int mColorDef; // 线段和圆圈颜色
private int mColorSelected; //选中的字体和圆圈颜色
private int mLineHight; //基准线高度
private int mCircleHight; //圆圈的高度(直径)
private int mCircleSelStroke; //被选中圆圈(空心)的粗细
private int mMarginTop; //圆圈和文字之间的距离
private String[] tabNames; //需要绘制的文字
private int mTextSize; //文本的字体大小
-->
<declare-styleable name="SlidTab">
<attr name="textColorDef" format="reference|color"/> <!--默认文本的颜色-->
<attr name="android:textSize"/> <!--文本的字体大小-->
<attr name="defColor" format="reference|color" /> <!--线段和圆圈颜色-->
<attr name="selectedColor" format="reference|color" /><!--选中的字体和圆圈颜色-->
<attr name="lintHight" format="dimension" /> <!--基准线高度-->
<attr name="circleHight" format="dimension" /> <!--圆圈的高度(直径)-->
<attr name="circleSelStroke" format="dimension" /> <!--被选中圆圈(空心)的粗细-->
<attr name="mMarginTop" format="dimension" /> <!--圆圈和文字之间的距离-->
<attr name="tabNames" format="reference" /> <!--需要绘制的文字-->
</declare-styleable>
</resources>
布局中使用:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:openXu="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dip">
<com.openxu.st.SlideTab
android:id="@+id/slideTab"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize = "15sp"
openXu:textColorDef = "#A4A4A4"
openXu:defColor = "#EAEAEA"
openXu:selectedColor = "#5CBB8C"
openXu:lintHight = "2dip"
openXu:circleHight = "20dip"
openXu:circleSelStroke = "5dip"
openXu:mMarginTop = "15dip"
openXu:tabNames = "@array/tab_names" />
</LinearLayout>
运行效果:
欢迎关注,希望在这里有你想要的,博主会持续更新高(di)质(ji)量(shu)的文章和大家交流学习,祝各位学习愉快。
喜欢请点赞,no爱请勿喷~O(∩_∩)O谢谢
源码下载:
注:没有积分的童鞋 请留言索要代码喔
Android自定义View实战(SlideTab-可滑动的选择器)的更多相关文章
- 转载:android自定义view实战(温度控制表)!
效果图 package cn.ljuns.temperature.view; import com.example.mvp.R; import android.content.Context;impo ...
- Android为TV端助力 转载:android自定义view实战(温度控制表)!
效果图 package cn.ljuns.temperature.view; import com.example.mvp.R; import android.content.Context;impo ...
- android自定义View之仿通讯录侧边栏滑动,实现A-Z字母检索
我们的手机通讯录一般都有这样的效果,如下图: OK,这种效果大家都见得多了,基本上所有的android手机通讯录都有这样的效果.那我们今天就来看看这个效果该怎么实现. 一.概述 1.页面功能分析 整体 ...
- Android 自定义View合集
自定义控件学习 https://github.com/GcsSloop/AndroidNote/tree/master/CustomView 小良自定义控件合集 https://github.com/ ...
- Android自定义View之ProgressBar出场记
关于自定义View,我们前面已经有三篇文章在介绍了,如果筒子们还没阅读,建议先看一下,分别是android自定义View之钟表诞生记.android自定义View之仿通讯录侧边栏滑动,实现A-Z字母检 ...
- android自定义View之NotePad出鞘记
现在我们的手机上基本都会有一个记事本,用起来倒也还算方便,记事本这种东东,如果我想要自己实现,该怎么做呢?今天我们就通过自定义View的方式来自定义一个记事本.OK,废话不多说,先来看看效果图. 整个 ...
- Android自定义View(CustomCalendar-定制日历控件)
转载请标明出处: http://blog.csdn.net/xmxkf/article/details/54020386 本文出自:[openXu的博客] 目录: 1分析 2自定义属性 3onMeas ...
- Android自定义View(RollWeekView-炫酷的星期日期选择控件)
转载请标明出处: http://blog.csdn.net/xmxkf/article/details/53420889 本文出自:[openXu的博客] 目录: 1分析 2定义控件布局 3定义Cus ...
- Android 自定义View——自定义点击事件
每个人手机上都有通讯录,这是毫无疑问的,我们通讯录上有一个控件,在通讯录的最左边有一列从”#”到”Z”的字母,我们通过滑动或点击指定的字母来确定联系人的位置,进而找到联系人.我们这一节就通过开发这个控 ...
随机推荐
- Spring MVC【入门】就这一篇!
MVC 设计概述 在早期 Java Web 的开发中,统一把显示层.控制层.数据层的操作全部交给 JSP 或者 JavaBean 来进行处理,我们称之为 Model1: 出现的弊端: JSP 和 Ja ...
- .Net Core 部署在win10 的IIS上注意问题。
事项一:_Layout.cshtml页面中<environment include="Development"></environment>里应用的样式无用 ...
- SpringBoot开发案例之多任务并行+线程池处理
前言 前几篇文章着重介绍了后端服务数据库和多线程并行处理优化,并示例了改造前后的伪代码逻辑.当然了,优化是无止境的,前人栽树后人乘凉.作为我们开发者来说,既然站在了巨人的肩膀上,就要写出更加优化的程序 ...
- [USACO 04OPEN]MooFest
Description 约翰的N 头奶牛每年都会参加“哞哞大会”.哞哞大会是奶牛界的盛事.集会上的活动很多,比如堆干草,跨栅栏,摸牛仔的屁股等等.它们参加活动时会聚在一起,第i 头奶牛的坐标为Xi,没 ...
- [LOJ 6249]「CodePlus 2017 11 月赛」汀博尔
Description 有 n 棵树,初始时每棵树的高度为 H_i,第 i 棵树每月都会长高 A_i.现在有个木料长度总量为 S 的订单,客户要求每块木料的长度不能小于 L,而且木料必须是整棵树(即不 ...
- Go学习——defer、panic
defer: 延迟到ret之前,通常用于IO的关闭 or 错误处理. 在延迟出现的异常可以被后面的捕捉,但是只有最后一个. defer可以多次,这样形成一个defer栈,后defer的语句在函数返回时 ...
- UVA1349:Optimal Bus Route Design
题意:给定一个有向带权图,找若干个环,使得每个点属于且仅属于一个环,要求使得环权值之和最小 题解:发现这题中每个点属于且仅属于一个环,这时候"仅"这种恰好的含义,让我们想到了匹配问 ...
- [HEOI 2016] seq
题解: 发现多决策且明显无后效性,果断dp,那么转移方程F[i]=F[j]+1 设R[I]为改变之后的最大值,L[i]为改变之后的最小值 由于只能改变一个元素 所以转移的条件是 (j<i &am ...
- [bzoj3673/3674可持久化并查集加强版]
n个集合 m个操作 操作: 1 a b 合并a,b所在集合 2 k 回到第k次操作之后的状态(查询算作操作) 3 a b 询问a,b是否属于同一集合,是则输出1否则输出0 0<n,m<=2 ...
- 谷歌发布 TensorFlow Lite [官方网站,文档]
机器学习社区:http://tensorflow123.com/ 简介 TensorFlow Lite TensorFlow Lite 是 TensorFlow 针对移动和嵌入式设备的轻量级解决方案. ...