Android开发技巧——写一个StepView
在我们的应用开发中,有些业务流程会涉及到多个步骤,或者是多个状态的转化,因此,会需要有相关的设计来展示该业务流程。比如《停车王》应用里的添加车牌的步骤。
通常,我们会把这类控件称为“StepView”。上图的这种设计相对来说还是比较简单的,下面我们以它为例,来一步步写我们的“StepView”。
那么,实现这样的一个“StepView”,我们会需要哪些知识呢?
所需知识
- 布局测量
- 图形文字绘制
- 文字位置计算
布局测量
首先像这样的StepView,它的宽度应该是填满或者是固定的,因为考虑到屏幕适配的关系,每一步之间的线的长度应该是自适应的。而它的高度,除了固定高度或填满父布局高度,我们还希望它可以根据自己的内容来自适应高度。这时候就需要用到测量了。
图形文字绘制
在这个控件中,我们会需要绘制实心圆、空心圆、矩形(每一步之间的连线),文字。
文字位置计算
我们需要计算文字的位置,使数字正好在圆内居中,以及下面的文字与圆的距离如我们所设。
属性及方法的设计
在StepView当中,需要设定一些属性,比如未选中时圆的颜色,文字的颜色,选中时的颜色,文字大小,圆大小,中间连线的宽度等等。所以我们至少需要自定义以下属性:
- 圆颜色
- 底部文字颜色
- 选中时的颜色
- 圆的填充半径
- 圆的边框宽度
- 线的宽度
- 底部文字大小
- 底部文字与圆的距离
另外,我们希望该控件至少提供以下方法:
- public void setSteps(List<String> steps) 设置步骤内容
- public void selectedStep(int step) 选择某一步
- public int getCurrentStep() 返回当前在哪一步
- public int getStepCount() 返回总步数
代码实现
下面我们来一步步实现。
首先创建一个类StepView,继承自View。
控件属性
然后在values/attrs.xml中创建一个declare-styleable,代码如下:
<declare-styleable name="StepView">
<attr name="svCircleColor" format="color"/>
<attr name="svTextColor" format="color"/>
<attr name="svSelectedColor" format="color"/>
<attr name="svFillRadius" format="dimension"/>
<attr name="svStrokeWidth" format="dimension"/>
<attr name="svLineWidth" format="dimension"/>
<attr name="svTextSize" format="dimension"/>
<attr name="svDrawablePadding" format="dimension"/>
</declare-styleable>
这里需要说明一下,declare-styleable中的name应该与我们的类的名字一致,这样在AndroidStudio写布局时,就会有这些属性的代码提示。
默认的Style
我们在写一个自定义控件时,应该尽可能地给出一些预设值来使它有一个默认的效果,并且这些预设值可以被覆盖。所以在这里我们也写一个Style,对上面的属性给定一个默认值。在values/styles.xml中添加以下代码:
<style name="StepView">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:background">@android:color/white</item>
<item name="android:paddingTop">8dp</item>
<item name="android:paddingBottom">8dp</item>
<item name="svCircleColor">#EEE</item>
<item name="svTextColor">#999</item>
<item name="svSelectedColor">#418AF9</item>
<item name="svFillRadius">11dp</item>
<item name="svStrokeWidth">3dp</item>
<item name="svLineWidth">4dp</item>
<item name="svTextSize">12sp</item>
<item name="svDrawablePadding">10dp</item>
</style>
Java代码实现
成员变量及构造方法
下面是成员变量的定义及构造方法的实现:
private static final int START_STEP = 1;
private final List<String> mSteps = new ArrayList<>();
private int mCurrentStep = START_STEP;
private int mCircleColor;
private int mTextColor;
private int mSelectedColor;
private int mFillRadius;
private int mStrokeWidth;
private int mLineWidth;
private int mDrawablePadding;
private Paint mPaint;
public StepView(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.StepView, 0, R.style.StepView);
mCircleColor = ta.getColor(R.styleable.StepView_svCircleColor, 0);
mTextColor = ta.getColor(R.styleable.StepView_svTextColor, 0);
mSelectedColor = ta.getColor(R.styleable.StepView_svSelectedColor, 0);
mFillRadius = ta.getDimensionPixelSize(R.styleable.StepView_svFillRadius, 0);
mStrokeWidth = ta.getDimensionPixelSize(R.styleable.StepView_svStrokeWidth, 0);
mLineWidth = ta.getDimensionPixelSize(R.styleable.StepView_svLineWidth, 0);
mDrawablePadding = ta.getDimensionPixelSize(R.styleable.StepView_svDrawablePadding, 0);
final int textSize = ta.getDimensionPixelSize(R.styleable.StepView_svTextSize, 0);
ta.recycle();
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
mPaint.setTextSize(textSize);
mPaint.setTextAlign(Paint.Align.CENTER);
if (isInEditMode()) {
String[] steps = {"Step 1", "Step 2", "Step 3"};
setSteps(Arrays.asList(steps));
}
}
首先,我们将需要在onDraw(Canvas canvas)方法中用到的属性值都作为成员变量定义,并且实现构造方法public StepView(Context context, AttributeSet attrs),以便我们能在布局中使用这个控件。
其次,需要注意的是,这里获取属性值的代码是context.obtainStyledAttributes(attrs, R.styleable.StepView, 0, R.style.StepView);,可参见我另一篇讲自定义View的博客《Android开发技巧——自定义控件之使用style》。这里简单解释一下,第三个参数是定义的Style属性,由于我们这里没有定义,所以传入的是0。第四个参数表示我们定义的Style资源,这里传入前面所写的style。在确定一个属性最终的值的时候,优先级顺序是这样的:
- 首先获取给定的AttributeSet中的属性值
- 如果找不到,则去AttributeSet中style(你在写布局文件时定义的style=”@style/xxxx”)指定的资源获取
- 如果找不到,则去defStyleAttr以及defStyleRes中的默认style中获取。
- 最后去找的是当前theme下的基础值。
获取到TypedArray对象之后就是各种取属性值,取完调用其recycle()方法回收。然后初始化我们的画笔,这里我调用了mPaint.setTextAlign(Paint.Align.CENTER);,让绘制时文字对齐方式为居中,主要是为了方便后面文字的计算与绘制。
在这个构造方法的最后,我还写了几行代码:
if (isInEditMode()) {
String[] steps = {"Step 1", "Step 2", "Step 3"};
setSteps(Arrays.asList(steps));
}
这个isInEditMode是在预览布局时使用的,它在布局预览时返回true,而当实际运行的时候则不会进入这个条件语句。因此我们可以利用其来设置一些数据,以便在AndroidStudio写布局时预览我们的控件效果。
基本行为实现
下面是实现我们在前面所设计的方法:
public void setSteps(List<String> steps) {
mSteps.clear();
if (steps != null) {
mSteps.addAll(steps);
}
selectedStep(START_STEP);
}
public void selectedStep(int step) {
final int selected = step < START_STEP ?
START_STEP : (step > mSteps.size() ? mSteps.size() : step);
mCurrentStep = selected;
invalidate();
}
public int getCurrentStep() {
return mCurrentStep;
}
public int getStepCount() {
return mSteps.size();
}
测量
接下来是测量。
这里的测量还是比较好理解的。我们仅需要对高度为wrap_content的情况进行计算。
在之前的博客《Android开发技巧——实现设计师给出的视觉居中的布局》中,我们知道wrap_content对应的是Java代码中的MeasureSpec.AT_MOST,所以这里在高度模式为MeasureSpec.AT_MOST时,计算我们的控件高度。它的高度为上下内边距加上外圆的直径,文字的大小以及文字与圆的距离。
代码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode == MeasureSpec.AT_MOST) {
final int fontHeight = (int) Math.ceil(mPaint.descent() - mPaint.ascent());
height = getPaddingTop() + getPaddingBottom() + (mFillRadius + mStrokeWidth) * 2
+ mDrawablePadding + fontHeight;
}
setMeasuredDimension(width, height);
}
绘制
接下来就是重写protected void onDraw(Canvas canvas)方法进行绘制了。
首先,如果步骤为空,是不需要绘制的:
final int stepSize = mSteps.size();
if (stepSize == 0) {
return;
}
接下来是绘制每一步的内容。
这里我们把绘制分为两部分,首先是绘制每一步的内容,其次是绘制每一步之间的连线。在这里我们需要知道如何计算文字的高度,以及绘制文字时的起始点。
下面是我在网上找的一张字体属性示意图。
在Android当中,文字的绘制是从Baseline开始的。下面是其中字体属性的说明:
- ascent 单个文字中所建议的在Baseline上面的距离,它是一个负值。
- descent 单个文字中所建议的在Baseline下面的距离,它是一个正值。
- leading 在每一行文字之间所建议的额外的空间
相关文档可参见:https://developer.android.google.cn/reference/android/graphics/Paint.FontMetrics.html
所以我们的文字高度为descent-ascent,文字中心与baseline的距离为-ascent - (-ascent + descent) / 2即-(ascent + descent) / 2。
绘制每一步我们需要计算字体的高度,字体中心与baseline的距离,大圆半径,圆心坐标,每一步的宽度。代码如下:
final int width = getWidth();
final float ascent = mPaint.ascent();
final float descent = mPaint.descent();
final int fontHeight = (int) Math.ceil(descent - ascent);
final int halfFontHeightOffset = -(int)(ascent + descent) / 2;
final int bigRadius = mFillRadius + mStrokeWidth;
final int startCircleY = getPaddingTop() + bigRadius;
final int childWidth = width / stepSize;
for (int i = 1; i <= stepSize; i++) {
drawableStep(canvas, i, halfFontHeightOffset, fontHeight, bigRadius,
childWidth * i - childWidth / 2, startCircleY);
}
其中绘制每一步的方法的代码如下:
private void drawableStep(Canvas canvas, int step, int halfFontHeightOffset, int fontHeight,
int bigRadius, int circleCenterX, int circleCenterY) {
final String text = mSteps.get(step - 1);
final boolean isSelected = step == mCurrentStep;
if (isSelected) {
mPaint.setStrokeWidth(mStrokeWidth);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(mCircleColor);
canvas.drawCircle(circleCenterX, circleCenterY, mFillRadius + mStrokeWidth / 2, mPaint);
mPaint.setColor(mSelectedColor);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawCircle(circleCenterX, circleCenterY, mFillRadius, mPaint);
} else {
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(mCircleColor);
canvas.drawCircle(circleCenterX, circleCenterY, bigRadius, mPaint);
}
mPaint.setFakeBoldText(true);
mPaint.setColor(Color.WHITE);
String number = String.valueOf(step);
canvas.drawText(number, circleCenterX, circleCenterY + halfFontHeightOffset, mPaint);
mPaint.setFakeBoldText(false);
mPaint.setColor(isSelected ? mSelectedColor : mTextColor);
canvas.drawText(text, circleCenterX,
circleCenterY + bigRadius + mDrawablePadding + fontHeight / 2, mPaint);
}
最后是绘制这些连线:
final int halfLineLength = childWidth / 2 - bigRadius;
for (int i = 1; i < stepSize; i++) {
final int lineCenterX = childWidth * i;
drawableLine(canvas, lineCenterX - halfLineLength,
lineCenterX + halfLineLength, startCircleY);
}
其中绘制每条线的方法代码如下:
private void drawableLine(Canvas canvas, int startX, int endX, int centerY) {
mPaint.setColor(mCircleColor);
mPaint.setStrokeWidth(mLineWidth);
canvas.drawLine(startX, centerY, endX, centerY, mPaint);
}
到这里,该控件已经完整实现。
下面是运行效果:
全部代码可参见对应的Github项目msdx/StepView:https://github.com/msdx/StepView
本文关联我的简书博客http://www.jianshu.com/p/bcfed38d1cb7,并已投稿至个人微信号“浩码农”,未经许可,不得转载。
Android开发技巧——写一个StepView的更多相关文章
- Android开发技巧——使用PopupWindow实现弹出菜单
在本文当中,我将会与大家分享一个封装了PopupWindow实现弹出菜单的类,并说明它的实现与使用. 因对界面的需求,android原生的弹出菜单已不能满足我们的需求,自定义菜单成了我们的唯一选择,在 ...
- Android开发技巧——实现可复用的ActionSheet菜单
在上一篇<Android开发技巧--使用Dialog实现仿QQ的ActionSheet菜单>中,讲了这种菜单的实现过程,接下来将把它改成一个可复用的控件库. 本文原创,转载请注明出处: h ...
- Android开发技巧——高亮的用户操作指南
Android开发技巧--高亮的用户操作指南 2015-12-15补记: 发现使用PopupWindow进行遮罩层的显示,在华为P7上会有问题.具体表现为:画出来的高亮部分会偏下.原因为:通过view ...
- Android开发技巧——自定义控件之增加状态
Android开发技巧--自定义控件之增加状态 题外话 这篇本该是上周四或上周五写的,无奈太久没写博客,前几段把我的兴头都用完了,就一拖再拖,直到今天.不想把这篇拖到下个月,所以还是先硬着头皮写了. ...
- Android开发技巧——自定义控件之使用style
Android开发技巧--自定义控件之使用style 回顾 在上一篇<Android开发技巧--自定义控件之自定义属性>中,我讲到了如何定义属性以及在自定义控件中获取这些属性的值,也提到了 ...
- Android开发技巧——自定义控件之自定义属性
Android开发技巧--自定义控件之自定义属性 掌握自定义控件是很重要的,因为通过自定义控件,能够:解决UI问题,优化布局性能,简化布局代码. 上一篇讲了如何通过xml把几个控件组织起来,并继承某个 ...
- Android开发技巧——自定义控件之组合控件
Android开发技巧--自定义控件之组合控件 我准备在接下来一段时间,写一系列有关Android自定义控件的博客,包括如何进行各种自定义,并分享一下我所知道的其中的技巧,注意点等. 还是那句老话,尽 ...
- Android开发技巧——大图裁剪
本篇内容是接上篇<Android开发技巧--定制仿微信图片裁剪控件> 的,先简单介绍对上篇所封装的裁剪控件的使用,再详细说明如何使用它进行大图裁剪,包括对旋转图片的裁剪. 裁剪控件的简单使 ...
- 50个android开发技巧
50个android开发技巧 http://blog.csdn.net/column/details/androidhacks.html
随机推荐
- yum速查
yum命令是在Fedora和RedHat以及SUSE中基于rpm的软件包管理器,它可以使系统管理人员交互和自动化地更细与管理RPM软件包, 能够从指定的服务器自动下载RPM包并且安装,可以自动处理依赖 ...
- python连接mongo
连接mongodb数据库 用到pymongo模块 应该是这样来使用: , 'goods') 然后连接数据库层这么写 def getSpinfo(item,value,depart,comp): res ...
- 再谈WinIO初始化异常
再谈WinIO初始化异常 前段时间WinIO在我的新项目中总是初始化失败,有时候又是好好的,很让人费解.修改了源代码显示了很多调试信息后,也没有什么太多的收获.由于我们的工控卡必须要用这个库, ...
- 解释一下python中的//,%和**运算符
//运算符执行地板除法(向下取整除),它会返回整除结果的整数部分 print(7//2) 这里整除后会返回3.5 同样的,执行取幂运算,ab会返回a的b次方 print(2**10) 最后,%执行取模 ...
- HackerRank - powers-game-1 【博弈论】
HackerRank - powers-game-1 [博弈论] 题意 给出 * 2^1 * 2^2 * 2^3 * 2^4 * 2^5 * 2^n 这一串东西 ,然后有两个玩家,*号是可以被替换掉的 ...
- 【转】PCA与Whitening
PCA: PCA的具有2个功能,一是维数约简(可以加快算法的训练速度,减小内存消耗等),一是数据的可视化. PCA并不是线性回归,因为线性回归是保证得到的函数是y值方面误差最小,而PCA是保证得到的函 ...
- jetbrains goland 跳到上一个光标处
查了下是 :Ctrl + Alt + 左右 mac下面是:Command+ Alt + 左右键 但是我用下来是切上面打开文档页 摸索了下是:Ctrl +Win+ Alt + 左右 我的键的映射是De ...
- ARM协处理器CP15寄存器详解【转】
本文转载i自;https://blog.csdn.net/gameit/article/details/13169405 用于系统存储管理的协处理器CP15 MCR{cond} copro ...
- 用adb 启动camera
adb shell am start -a android.media.action.STILL_IMAGE_CAMERA 启动camera adb shell input keyevent 27 ...
- 关于camera 构架设计的一点看法
camera的构架目前来看有两种,一种是集中式管理,比如说建立一个引擎,引擎向上提供接口,向下管理所有模块.把camera的所有功能划分为不同的模块,又引擎统一管理.模块的结构就比较随意了,可以统一接 ...