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,并已投稿至个人微信号“浩码农”,未经许可,不得转载。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
苹果操作系统曝最新安全漏洞,近亿部设备或被黑
据CNET报道,思科公司的研究人员发现,除了苹果发布的最新版本操作系统,此前的所有版本的iOS或OS X都存在共同的安全漏洞,只要你发送一条默认的iMessage信息,黑客就可以侵入你的设备。举例来说,通过发给你感染病毒的iMessage,黑客就可以获得你的密码,他们唯一需要的就是你的手机号码。 这个漏洞是思科公司旗下网络安全部门Cisco Talos的研究员泰勒·博安(Tyler Bohan)发现的。首先,黑客会开发含有恶意代码的TIF(类似JPG或GIF的图片格式)文件,然后通过苹果即时通信应用iMessage发送给目标。这种方法特别有效,因为iMessage会自动渲染其默认设置中的图片。 当你接收都被感染的文件时,恶意代码就会在目标设备上执行,让黑客可以访问你设备的内存,并获得储存的密码。受害者甚至没有机会预防这种攻击。同样的攻击还可通过电子邮件或诱使用户使用苹果浏览器Safari访问含有病毒图片的网站发动。 更糟糕的是,博安还发现这个安全漏洞几乎存在于所有版本的苹果操作系统中,除了7月18日发布的iOS 9.3.3或El Capitan 10.11.6。博安已经与苹果分享了自己...
- 下一篇
Java 程序员的错
Java程序员是有问题的。我使用Java编程已经有10多年的历史。同时,我还有过大量的使用其它语言开发的经历,比如C#, C, C++, Python, Lua, Objective-C等等,我认为这些经历在对我认识Java程序员的问题上起到了巨大的帮助。很多人说Java是一种很糟糕的编程语言。我不同意。 Java语言有它自己的缺点,但我想,很多时候,当你看到Java在有些地方让人很多人不爽时,那本质上不是Java语言的问题,而是它被错误的使用。 这些年来,在我见过的各种Java代码中,我发现这最大的问题是,写代码的人痴迷于把自己当作架构师。他们很喜欢这样,在我阅读他们的代码时,经常会发现这些代码与其说是去真正的解决一个问题,事实上更像是为了解决一个问题而规划的一个蓝图模板。这两者之间并不是细微的差别。你会看到继承很深的抽象 层和成堆臃肿的样板式的代码。由面向对象而诞生的子类超生现象无以复加。你根本无法一眼看明白、理解这些代码是干什么的——你需要一层层深入 挖掘,你需要理解它的整套滥用的术语和折磨人的词汇(“AbstractAdapterFactory”),你必须要把自己当成系统的一部...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Linux系统CentOS6、CentOS7手动修改IP地址
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- Red5直播服务器,属于Java语言的直播服务器
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS8编译安装MySQL8.0.19
- CentOS7,CentOS8安装Elasticsearch6.8.6
- SpringBoot2整合Redis,开启缓存,提高访问速度
- CentOS6,CentOS7官方镜像安装Oracle11G
- Windows10,CentOS7,CentOS8安装Nodejs环境