Android -- 打造我们的StepView
1,前两天我们分析了Github开源的StepView 《自定义StepView实现个人信息验证进度条》,这两天想着想自己写一个,so,就有了这一篇文章,不废话,先看看实现的效果:
2,首先我们来看看我们常规的自定义view的基础步骤吧
1,继承View,重写构造方法
2,自定义属性
3,重写onMeasure()测量控件高度
4,重写onDraw()绘制子view
- 初步分析
首先根据我们的上面效果,可以看到,主要是由直线、圆环、下面的文字组成,所以我们打算使用这三种view组合来形成我们上面的效果
- 准备工作
①首先我们要提供一个装置下面文字的集合texts,我们文字有文字的大小属性mTextSize、正常文字颜色mColorTextDefault、文字被选中时的颜色mColorTextSelect,最后还有文字距离上面圆环的距离mMarginTop
②然后我们提供相关的圆环相关的属性,圆的半径mCircleRadius、圆环被选中的颜色mColorCircleSelect、圆环正常时的颜色mColorCircleDefault
③再看看我们链接圆弧之间的直线属性,直线的长度mLineLength、直线的高度mLineHeight,颜色和我们圆环默认颜色相同,就不用重新定义了
④还有一些需要定义的属性,例如当前被选中的位置mSelectPosition,每一个测量的TextView保存的Rect的集合mBounds,还有各种画笔
所以我们就可以开始写一写代码了,首先创建StepView继承View,然后初始化数据,并测量TextView,将测量信息保存在mBounds集合中
public class SlideStepView extends View {
//先分析我们这次需要哪些预备的属性 //存放下面文字集合
private List<String> texts;
//文字大小
private int mTextSize;
//文字常规颜色
private int mColorTextDefault;
//文字被选择时候的颜色
private int mColorTextSelect;
//圆和文字之间的距离
private int mMarginTop;
//线段和圆圈常规的颜色
private int mColorCircleDefault;
//圆圈被选中的的颜色
private int mColorCircleSelect;
//中间线段的整个长度
private float mLineLength;
//中间线段宽度
private int mLineHeight;
//圆圈的半径
private int mCircleRadius;
//选中后蓝色的宽度
private int mSelectCircleStroke;
//当前选中的下标
private int mSelectPosition; //保存每个TextView的测量矩形数据
private List<Rect> mBounds; //各种画笔
private Paint mTextPaint;
private Paint mLinePaint;
private Paint mCirclePaint;
private Paint mCircleSelectPaint; public SlideStepView(Context context) {
this(context, null);
} public SlideStepView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
} public SlideStepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr); //初始化数基本属性
init();
} private void init() {
//初始化数据源容器
texts = new ArrayList<>();
mBounds = new ArrayList<>(); //添加加数据
texts.add("订单已支付");
texts.add("商家已接单");
texts.add("骑手已接单");
texts.add("订单已送达"); //将当前选中为2
mSelectPosition = 1;
mMarginTop = 20;
mCircleRadius = 30;
mSelectCircleStroke = 3; //初始化文字属性
mColorTextDefault = Color.GRAY;
mColorTextSelect = Color.BLUE;
mTextSize = 20;
mTextPaint = new Paint();
mTextPaint.setTextSize(mTextSize);
mTextPaint.setColor(mColorTextDefault);
mTextPaint.setAntiAlias(true); //初始化圆圈属性
mColorCircleDefault = Color.argb(255, 234, 234, 234); mCirclePaint = new Paint();
mCirclePaint.setColor(mColorCircleDefault);
mCirclePaint.setStyle(Paint.Style.FILL);
mCirclePaint.setAntiAlias(true); //初始化被选中的圆圈
mColorCircleSelect = Color.BLUE;
mCircleSelectPaint = new Paint();
mCircleSelectPaint.setColor(mColorCircleSelect);
mCircleSelectPaint.setStyle(Paint.Style.FILL);
mCircleSelectPaint.setAntiAlias(true);
// mCircleSelectPaint.setStrokeWidth(mSelectCircleStroke); //设置线段属性
mLineHeight = 5;
mLinePaint = new Paint();
mLinePaint.setColor(mColorCircleDefault);
mLinePaint.setStyle(Paint.Style.FILL);
mLinePaint.setStrokeWidth(mLineHeight);
mLinePaint.setAntiAlias(true); //测量TextView
measureText();
} private void measureText() {
for (int i = 0; i < texts.size(); i++) {
Rect rect = new Rect();
mTextPaint.getTextBounds(texts.get(i), 0, texts.get(i).length(), rect);
mBounds.add(rect);
}
}
}
然后在onChangeSize中计算出mLineLength的长度(这里很简单 getWidth() - paddingLeft -paddingRight -2*mCircleRadius),重写onDraw()方法
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh); //计算线段整条线段长度(总控件宽度 - Padding - 最左边和最右边的两个圆的直径)
mLineLength = getWidth() - getPaddingLeft() - getPaddingRight() - mCircleRadius * 2;
} /**
* 绘制view
*
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
//绘制线条
canvas.drawLine(mCircleRadius, mCircleRadius, getWidth() - mCircleRadius, mCircleRadius, mLinePaint); //开是循环绘制view
for (int i = 0; i < texts.size(); i++) {
mTextPaint.setColor(mColorCircleDefault);
if (mSelectPosition == i) {
//绘制选中的圆圈
canvas.drawCircle(mCircleRadius + ((mLineLength / (texts.size() - 1)) * i), mCircleRadius, mCircleRadius, mCircleSelectPaint);
mTextPaint.setColor(mColorCircleSelect);
} else {
//绘制默中的圆圈
canvas.drawCircle(mCircleRadius + ((mLineLength / (texts.size() - 1)) * i), mCircleRadius, mCircleRadius, mCirclePaint);
}
//绘制文字
int startTextY = mCircleRadius * 2 + mMarginTop + getPaddingTop();
if (i == 0) {
canvas.drawText(texts.get(i), 0, startTextY, mTextPaint);
} else if (i == texts.size() - 1) {
canvas.drawText(texts.get(i), getWidth() - mBounds.get(i).width(), startTextY, mTextPaint);
} else {
canvas.drawText(texts.get(i), mCircleRadius + ((mLineLength / (texts.size() - 1)) * i) - (mBounds.get(i).width() / 2), startTextY, 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.qianmo.activitydetail.view.SlideStepView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#aaff0000"/>
</LinearLayout>
这样应该可以实现基本效果了,看一看我们实现的效果
- 重写onMeasure,改变测量的高度
这里我们可以看到当我们设置我们控件的高度为wrap_content,控件缺填充了整个屏幕,这一点我们在之前的《onMeasure()源码分析》写过,没有了解过的同学,大家可以去看一下,所以我们要修改onMeasure中的方法
/**
* 重写测量方式
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int height;
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
height = mMarginTop + 2 * mCircleRadius + mBounds.get(0).height();
//高度
Log.i("wangjitao:", "mMarginTop:" + mMarginTop + ",mCircleRadius:" + mCircleRadius + ",mBounds:"
+ mBounds.get(0).height() + ",height" + height);
}
//保存测量结果
setMeasuredDimension(widthSize, height);
}
再看一下我们的运行效果
- 对canvas.drawText()方法进行理解
我们这时候将我们前面的init()方法中的mMarginTop修改为0,mMarginTop代表下面文字距离上面圆环的距离,设置为0的话就表示我们的文字的text刚好贴在这个圆环的下面,但是实际效果不是这个样子的,看一下运行的效果
这里我们可以看到我们的文字和我们的圆弧重叠了,这是为什么呢? 我们的代码逻辑也问题啊,为什么会出现这个问题呢?我们下来看一下下面这张text的展示图就知道了
上面所有的属性都被封装在FontMetrics类中,通过它可以获取并计算文本的宽高,大体翻译一下,可能不准确;
top:在一个大小确定的字体中,被当做最高字形,基线(base)上方的最大距离。
ascent:单行文本中,在基线(base)上方被推荐的距离。
descent:单行文本中,在基线(base)下方被推荐的距离。
bottom:在一个大小确定的字体中,被当做最低字形,基线(base)下方的最大距离。
这是我们自定义View中text的一些属性,有人会问,楼猪啊 ,为什么要让我们了解这个些知识呢?因为我们的上面出的重叠问题就是这一点的问题,在我们的正常思维的认知中我们的canvas.drawText的第三个参数是Y坐标的起始点,而我们上面的代码Y坐标的计算方式是 startTextY = mCircleRadius * 2 + mMarginTop + getPaddingTop();我们的主观思维也感觉没问题,但是让我们看一下canvas.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);
}
看到没有“@param y The y-coordinate of the baseline of the text being drawn” 这个方法中我们的y参数表示我们的baseline,而不是我们之前的想当然的test的top属性,所以我们要修改startTextY 的计算方式为
//这里要对基线进行理解
int startTextY = mCircleRadius * 2 + mMarginTop + getPaddingTop(); //以前
Log.i("wangjitao", "以前:" + startTextY);
//现在是这样的,首先获取基线对象
Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
startTextY = getHeight() - (int) fontMetrics.bottom;
ok,再看看我们的运行效果
没什么问题了
- 重写onTouch()方实现侧滑更换当前选中位置
这个没什么好讲的,就是向左滑动和向右滑动改变当前选中位置而已,代码如下:
private float downX;
private float upX; @Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
//按下手指的时候记录下按下的位置
case MotionEvent.ACTION_DOWN:
Log.e("wangjitao", "手指按下: getX:" + downX);
downX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
Log.i("wangjitao", "手指滑动: ");
break;
case MotionEvent.ACTION_UP:
upX = event.getX();
Log.e("wangjitao", "手指抬起: " + upX);
if (downX - upX > 50) {
downX = 0;
upX = 0;
//向左滑动
//判断做滑动的时候当前选择点时候在在初始状态下
if (mSelectPosition != 0) {
//更新view
mSelectPosition--;
} else {
mSelectPosition = texts.size() - 1;
}
invalidate();
} else if (upX - downX > 50) {
//向右滑动
downX = 0;
upX = 0;
//判断做滑动的时候当前选择点时候在最后一个点上
if (mSelectPosition != texts.size() - 1) {
//更新view
mSelectPosition++;
} else {
mSelectPosition = 0;
}
invalidate();
} else {
downX = 0;
upX = 0;
}
break;
}
return true;
}
再把最后所有的代码贴出来
package com.qianmo.activitydetail.view; import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View; import java.util.ArrayList;
import java.util.List; /**
* Created by wangjitao on 2017/3/24 0024.
* E-Mail:543441727@qq.com
* 自定义view实现StepView的实现
*/ public class SlideStepView extends View {
//先分析我们这次需要哪些预备的属性 //存放下面文字集合
private List<String> texts;
//文字大小
private int mTextSize;
//文字常规颜色
private int mColorTextDefault;
//文字被选择时候的颜色
private int mColorTextSelect;
//圆和文字之间的距离
private int mMarginTop;
//线段和圆圈常规的颜色
private int mColorCircleDefault;
//圆圈被选中的的颜色
private int mColorCircleSelect;
//中间线段的整个长度
private float mLineLength;
//中间线段宽度
private int mLineHeight;
//圆圈的半径
private int mCircleRadius;
//选中后蓝色的宽度
private int mSelectCircleStroke;
//当前选中的下标
private int mSelectPosition; //保存每个TextView的测量矩形数据
private List<Rect> mBounds; //各种画笔
private Paint mTextPaint;
private Paint mLinePaint;
private Paint mCirclePaint;
private Paint mCircleSelectPaint; public SlideStepView(Context context) {
this(context, null);
} public SlideStepView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
} public SlideStepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr); //初始化数基本属性
init();
} private void init() {
//初始化数据源容器
texts = new ArrayList<>();
mBounds = new ArrayList<>(); //添加加数据
texts.add("订单已支付");
texts.add("商家已接单");
texts.add("骑手已接单");
texts.add("订单已送达"); //将当前选中为2
mSelectPosition = 1;
mMarginTop = 0;
mCircleRadius = 30;
mSelectCircleStroke = 3; //初始化文字属性
mColorTextDefault = Color.GRAY;
mColorTextSelect = Color.BLUE;
mTextSize = 20;
mTextPaint = new Paint();
mTextPaint.setTextSize(mTextSize);
mTextPaint.setColor(mColorTextDefault);
mTextPaint.setAntiAlias(true); //初始化圆圈属性
mColorCircleDefault = Color.argb(255, 234, 234, 234); mCirclePaint = new Paint();
mCirclePaint.setColor(mColorCircleDefault);
mCirclePaint.setStyle(Paint.Style.FILL);
mCirclePaint.setAntiAlias(true); //初始化被选中的圆圈
mColorCircleSelect = Color.BLUE;
mCircleSelectPaint = new Paint();
mCircleSelectPaint.setColor(mColorCircleSelect);
mCircleSelectPaint.setStyle(Paint.Style.FILL);
mCircleSelectPaint.setAntiAlias(true);
// mCircleSelectPaint.setStrokeWidth(mSelectCircleStroke); //设置线段属性
mLineHeight = 5;
mLinePaint = new Paint();
mLinePaint.setColor(mColorCircleDefault);
mLinePaint.setStyle(Paint.Style.FILL);
mLinePaint.setStrokeWidth(mLineHeight);
mLinePaint.setAntiAlias(true); //测量TextView
measureText();
} private void measureText() {
for (int i = 0; i < texts.size(); i++) {
Rect rect = new Rect();
mTextPaint.getTextBounds(texts.get(i), 0, texts.get(i).length(), rect);
mBounds.add(rect);
}
} /**
* 重写测量方式
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int height;
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
height = mMarginTop + 2 * mCircleRadius + mBounds.get(0).height();
//高度
Log.i("wangjitao:", "mMarginTop:" + mMarginTop + ",mCircleRadius:" + mCircleRadius + ",mBounds:"
+ mBounds.get(0).height() + ",height" + height);
}
//保存测量结果
setMeasuredDimension(widthSize, height);
} @Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh); //计算线段整条线段长度(总控件宽度 - Padding - 最左边和最右边的两个圆的直径)
mLineLength = getWidth() - getPaddingLeft() - getPaddingRight() - mCircleRadius * 2;
} /**
* 绘制view
*
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
//绘制线条
canvas.drawLine(mCircleRadius, mCircleRadius, getWidth() - mCircleRadius, mCircleRadius, mLinePaint); //开是循环绘制view
for (int i = 0; i < texts.size(); i++) {
mTextPaint.setColor(mColorCircleDefault);
if (mSelectPosition == i) {
//绘制选中的圆圈
canvas.drawCircle(mCircleRadius + ((mLineLength / (texts.size() - 1)) * i), mCircleRadius, mCircleRadius, mCircleSelectPaint);
mTextPaint.setColor(mColorCircleSelect);
} else {
//绘制默中的圆圈
canvas.drawCircle(mCircleRadius + ((mLineLength / (texts.size() - 1)) * i), mCircleRadius, mCircleRadius, mCirclePaint);
}
//绘制文字
//这里要对基线进行理解
int startTextY = mCircleRadius * 2 + mMarginTop + getPaddingTop(); //以前
Log.i("wangjitao", "以前:" + startTextY);
//现在是这样的,首先获取基线对象
Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
startTextY = getHeight() - (int) fontMetrics.bottom;
Log.i("wangjitao", "现在:" + startTextY);
if (i == 0) {
canvas.drawText(texts.get(i), 0, startTextY, mTextPaint);
} else if (i == texts.size() - 1) {
canvas.drawText(texts.get(i), getWidth() - mBounds.get(i).width(), startTextY, mTextPaint);
} else { canvas.drawText(texts.get(i), mCircleRadius + ((mLineLength / (texts.size() - 1)) * i) - (mBounds.get(i).width() / 2), startTextY, mTextPaint);
}
}
} private float downX;
private float upX; @Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
//按下手指的时候记录下按下的位置
case MotionEvent.ACTION_DOWN:
Log.e("wangjitao", "手指按下: getX:" + downX);
downX = event.getX();
break;
case MotionEvent.ACTION_MOVE:
Log.i("wangjitao", "手指滑动: ");
break;
case MotionEvent.ACTION_UP:
upX = event.getX();
Log.e("wangjitao", "手指抬起: " + upX);
if (downX - upX > 50) {
downX = 0;
upX = 0;
//向左滑动
//判断做滑动的时候当前选择点时候在在初始状态下
if (mSelectPosition != 0) {
//更新view
mSelectPosition--;
} else {
mSelectPosition = texts.size() - 1;
}
invalidate();
} else if (upX - downX > 50) {
//向右滑动
downX = 0;
upX = 0;
//判断做滑动的时候当前选择点时候在最后一个点上
if (mSelectPosition != texts.size() - 1) {
//更新view
mSelectPosition++;
} else {
mSelectPosition = 0;
}
invalidate();
} else {
downX = 0;
upX = 0;
}
break;
}
return true;
}
}
运行效果
- 添加自定义属性
这里我们把好多控件的属性都写死了,我们可以用自定义属性来实现布局文件中动态的改变的,不了解的同学可以看我之前的《深入了解自定义属性》,这里就不一起写了,See You····
Android -- 打造我们的StepView的更多相关文章
- Android 打造炫目的圆形菜单 秒秒钟高仿建行圆形菜单
原文:Android 打造炫目的圆形菜单 秒秒钟高仿建行圆形菜单 转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/43131133, ...
- Android 打造自己的ImageLoader
Android 打造自己的ImageLoader 学习和参考 Android开发艺术探索 https://blog.csdn.net/column/details/15318.html 郭霖大神的Gl ...
- Android 打造完美的侧滑菜单/侧滑View控件
概述 Android 打造完美的侧滑菜单/侧滑View控件,完全自定义实现,支持左右两个方向弹出,代码高度简洁流畅,兼容性高,控件实用方便. 详细 代码下载:http://www.demodashi. ...
- Android打造属于自己的数据库操作类。
1.概述 开发Android的同学都知道sdk已经为我们提供了一个SQLiteOpenHelper类来创建和管理SQLite数据库,通过写一个子类去继承它,就可以方便的创建.管理数据库.但是当我们需要 ...
- android打造万能的适配器(转)
荒废了两天,今天与大家分享一个ListView的适配器 前段时间在学习慕课网的视频,觉得这种实现方式较好,便记录了下来,最近的项目中也使用了多次,节省了大量的代码,特此拿来与大家分享一下. 还是先看图 ...
- Android 打造形形色色的进度条 实现可以如此简单
转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/43371299 ,本文出自:[张鸿洋的博客] 1.概述 最近需要用进度条,秉着不重 ...
- Android 打造任意层级树形控件 考验你的数据结构和设计
转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/40212367,本文出自:[张鸿洋的博客] 1.概述 大家在项目中或多或少的可能会 ...
- Android 打造编译时注解解析框架 这只是一个开始
转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/43452969 ,本文出自:[张鸿洋的博客] 1.概述 记得很久以前,写过几篇博客 ...
- Android 打造任意层级树形控件 考验你的数据结构和设计
转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/40212367,本文出自:[张鸿洋的博客] 1.概述 大家在项目中或多或少的可能会 ...
随机推荐
- DACLs and ACEs
[Windows]Windows的访问控制模型 - Zplutor - 博客园 https://www.cnblogs.com/zplutor/archive/2010/01/05/1639892.h ...
- [dpdk] TSC , HPET, Timer, Event Timer,RDTSCP
关于dpdk timer跨越CPU core调度的准确性问题 首先dpdk的timer接口里边使用 cpu cycle来比较时间.根据之前的内容 [dpdk] dpdk --lcores参数 当一个E ...
- [administrative][archlinux][netctl][wpa_supplicant] 查看WIFI链接信息
当使用 netctl + wpa_supplicant的时候,环境里同时存在好多个WIFI,而我同时有配置了他们的密钥信息在配置文件中. 那么我该如何查看,我当前链接到了哪个WIFI上? /home/ ...
- C# decimal指定精度
最近做一个项目.遇到了decimal 如何指定精度的问题 一般的指定参数 param = new SqlParameter(ParamName, DbType); 但decimal就不能只通过构 ...
- 搭建Kubernetes服务集群遇到的问题
kube-proxy问题: Apr 12 09:42:49 compute1 kube-proxy[12965]: E0412 09:42:49.602342 12965 reflector.go:2 ...
- springboot 整合swagger-ui
一.添加maven依赖 <dependency> <groupId>io.springfox</groupId> <artifactId>springf ...
- django时间的时区问题(转)
add by zhj: 使用django时,如果设置USE_TZ=True,那django在数据库中存储的是0时区的时间:如果USE_TZ=False,那存储的是本地时间 原文:https://www ...
- 在asp.net中使用瀑布流,无限加载
页面中代码 <%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm1 ...
- es分词器
1.默认的分词器 standard standard tokenizer:以单词边界进行切分standard token filter:什么都不做lowercase token filter:将所有字 ...
- 关于安装SVN Service 出错 Service 'VisualSVN Server' failed to start. Please check VisualSVN Server log in Event Viewer for more details
关于安装SVN Service 出错 Service 'VisualSVN Server' failed to start. Please check VisualSVN Server log in ...