Android开发技巧——写一个StepView

简介: 在我们的应用开发中,有些业务流程会涉及到多个步骤,或者是多个状态的转化,因此,会需要有相关的设计来展示该业务流程。比如《停车王》应用里的添加车牌的步骤。 通常,我们会把这类控件称为“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,并已投稿至个人微信号“浩码农”,未经许可,不得转载。

目录
相关文章
|
24天前
|
Java Android开发
Android 开发获取通知栏权限时会出现两个应用图标
Android 开发获取通知栏权限时会出现两个应用图标
12 0
|
1月前
|
XML 缓存 Android开发
Android开发,使用kotlin学习多媒体功能(详细)
Android开发,使用kotlin学习多媒体功能(详细)
101 0
|
1月前
|
设计模式 人工智能 开发工具
安卓应用开发:构建未来移动体验
【2月更文挑战第17天】 随着智能手机的普及和移动互联网技术的不断进步,安卓应用开发已成为一个热门领域。本文将深入探讨安卓平台的应用开发流程、关键技术以及未来发展趋势。通过分析安卓系统的架构、开发工具和框架,本文旨在为开发者提供全面的技术指导,帮助他们构建高效、创新的移动应用,以满足不断变化的市场需求。
18 1
|
1月前
|
机器学习/深度学习 调度 Android开发
安卓应用开发:打造高效通知管理系统
【2月更文挑战第14天】 在移动操作系统中,通知管理是影响用户体验的关键因素之一。本文将探讨如何在安卓平台上构建一个高效的通知管理系统,包括服务、频道和通知的优化策略。我们将讨论最新的安卓开发工具和技术,以及如何通过这些工具提高通知的可见性和用户互动性,同时确保不会对用户造成干扰。
33 1
|
1天前
|
Linux 编译器 Android开发
FFmpeg开发笔记(九)Linux交叉编译Android的x265库
在Linux环境下,本文指导如何交叉编译x265的so库以适应Android。首先,需安装cmake和下载android-ndk-r21e。接着,下载x265源码,修改crosscompile.cmake的编译器设置。配置x265源码,使用指定的NDK路径,并在配置界面修改相关选项。随后,修改编译规则,编译并安装x265,调整pc描述文件并更新PKG_CONFIG_PATH。最后,修改FFmpeg配置脚本启用x265支持,编译安装FFmpeg,将生成的so文件导入Android工程,调整gradle配置以确保顺利运行。
16 1
FFmpeg开发笔记(九)Linux交叉编译Android的x265库
|
15天前
|
XML 开发工具 Android开发
构建高效的安卓应用:使用Jetpack Compose优化UI开发
【4月更文挑战第7天】 随着Android开发不断进化,开发者面临着提高应用性能与简化UI构建流程的双重挑战。本文将探讨如何使用Jetpack Compose这一现代UI工具包来优化安卓应用的开发流程,并提升用户界面的流畅性与一致性。通过介绍Jetpack Compose的核心概念、与传统方法的区别以及实际集成步骤,我们旨在提供一种高效且可靠的解决方案,以帮助开发者构建响应迅速且用户体验优良的安卓应用。
|
18天前
|
监控 算法 Android开发
安卓应用开发:打造高效启动流程
【4月更文挑战第5天】 在移动应用的世界中,用户的第一印象至关重要。特别是对于安卓应用而言,启动时间是用户体验的关键指标之一。本文将深入探讨如何优化安卓应用的启动流程,从而减少启动时间,提升用户满意度。我们将从分析应用启动流程的各个阶段入手,提出一系列实用的技术策略,包括代码层面的优化、资源加载的管理以及异步初始化等,帮助开发者构建快速响应的安卓应用。
|
18天前
|
Java Android开发
Android开发之使用OpenGL实现翻书动画
本文讲述了如何使用OpenGL实现更平滑、逼真的电子书翻页动画,以解决传统贝塞尔曲线方法存在的卡顿和阴影问题。作者分享了一个改造后的外国代码示例,提供了从前往后和从后往前的翻页效果动图。文章附带了`GlTurnActivity`的Java代码片段,展示如何加载和显示书籍图片。完整工程代码可在作者的GitHub找到:https://github.com/aqi00/note/tree/master/ExmOpenGL。
19 1
Android开发之使用OpenGL实现翻书动画
|
18天前
|
Android开发 开发者
Android开发之OpenGL的画笔工具GL10
这篇文章简述了OpenGL通过GL10进行三维图形绘制,强调颜色取值范围为0.0到1.0,背景和画笔颜色设置方法;介绍了三维坐标系及与之相关的旋转、平移和缩放操作;最后探讨了坐标矩阵变换,包括设置绘图区域、调整镜头参数和改变观测方位。示例代码展示了如何使用这些方法创建简单的三维立方体。
15 1
Android开发之OpenGL的画笔工具GL10
|
24天前
|
Android开发
Android开发小技巧:怎样在 textview 前面加上一个小图标。
Android开发小技巧:怎样在 textview 前面加上一个小图标。
12 0