65.9K
CodeProject 正在变化。 阅读更多。
Home

Android中的圆形进度条

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2020年10月19日

CPOL

9分钟阅读

viewsIcon

14117

downloadIcon

216

本文介绍了自定义视图的概念并实现了一个圆形进度条。

引言

Android 框架提供了多个默认视图,负责布局、测量和绘制自身及其相关子视图。视图还可以负责保存状态和操纵其 UI 属性。Android UI 元素都基于 View 类,该类是这些对象的基类。其中一些是内置视图,如“Button”和“TextView”。

有时,根据应用程序的要求,非常有必要创建自定义视图(您自己的 View)。自定义视图需要一些相关背景知识,这些知识将在本文中介绍。自定义视图涉及几个主要方面,从绘制开始,到存储结束。这些不同的方面应该同时加以利用,才能开发出行为良好、交互恰当的自定义视图。这些方面可以简要列出如下:

  • 绘图
  • 交互
  • 测量
  • 布局
  • 属性

上述方面在本文的 背景 章节中得到了充分介绍。理解这些方面的概念可以解答在开发自定义视图(如圆形进度条)过程中可能出现的许多问题。

图 1:圆形进度条的两个实例

圆形进度条是一种具有圆形形状的进度条,可以在一些特定应用程序中使用。图 1 展示了这两种进度条的实现版本。

开发的进度条可以轻松地集成到其他应用程序中,这一点在本文中有详细解释。

背景

正如在 引言 部分所述,自定义视图需要一些主要方面,这些方面在本节中已分别提及。在介绍这些方面之前,以下几点非常重要:

首先,在屏幕上显示一个简单的视图是一个层次结构,根节点首先显示,然后按顺序遍历其子节点。这意味着视图的父级在屏幕上比其对应的子视图先显示。需要注意的是,这种屏幕可视化速度非常快,您可能无法察觉这种按顺序遍历。

所有视图都应知道如何绘制、测量和布局自身,无论是内置视图还是自定义视图。这些方法是单独且独立调用的,以在屏幕上显示视图。绘制、测量和布局是设计自定义视图最重要的部分,在本节中已完全定义和解释。

可以像下面这样轻松定义一个自定义视图类:

public class myCustomView extends View {    

    public myCustomView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
}

上面的代码定义了一个空的自定义视图,其各个部分需要分别定义。接下来,将逐一介绍主要方面及其实现细节。

绘图

视图在屏幕上的渲染在此部分控制。重写 onDraw() 方法使我们能够描绘视图的整个视觉部分。此方法利用 Canvas 对象绘制视图的视觉形状,例如自定义视图的实例。例如,您可以使用此方法绘制圆形、线条和弧线,这些是您自己自定义视图的视觉部分。需要注意的是,此方法对于所有视图,包括自定义视图和内置视图,都以类似的方式进行评估。

Canvas 对象是一个简单的 2D 渲染工具,用于绘制形状,如下面的代码块所示,可用于绘制一个简单的圆形:

@Override
protected void onDraw(Canvas canvas){
    Paint paint=new Paint();
    paint.setColor(Color.BLACK);
    canvas.drawCircle(centerX,centerY,radius,paint);    
}

上面的代码在指定的中心和半径周围绘制了一个黑色的圆。paint 对象描述了绘制过程的一些样式,包括颜色、字体大小等。canvas 对象能够绘制一组简单的形状,您可以通过简单的网络搜索找到这些形状的列表。

测量

测量处理视图的尺寸,包括其 widthheight。这部分由应该被重写的 onMeasure() 方法进行评估。此方法根据其内容和父级的约束来确定视图的 widthheight(尺寸)。此方法有两个整数输入参数,分别是测量的特定 widthheight

重写的方法在期望尺寸和特定(强制)尺寸之间进行权衡。期望尺寸是在设计过程中给出的尺寸(在 XML 文件中手动设计),而特定(强制)尺寸由方法的输入参数给出。必须强调存在不同的模式,如下所示:

  • 精确模式:在此模式下,视图被强制具有特定的尺寸。视图必须具有强制尺寸,这可能通过考虑 match_parent 属性来实现。
  • 最大模式:当需要考虑最大尺寸时,会出现此模式。在此模式下,视图不能大于强制尺寸。
  • 不确定模式:视图可以自由拥有期望的尺寸,在此模式下不设置特定的宽度和高度值。当 widthheight 都设置为 wrap_content 时,会出现此模式。

为了更好地理解,以下是一个简单的方法形式:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(heightMeasureSpec);

    int width, height;

    if (widthMode == MeasureSpec.EXACTLY) {
        width = widthMeasureSpec;
    } else if (widthMode == MeasureSpec.AT_MOST) {
        width = Math.min(desiredWidth, widthMeasureSpec);
    } else {
        width = desiredWidth;
    }

    if (heightMode == MeasureSpec.EXACTLY) {
        height = heightMeasureSpec;
    } else if (heightMode == MeasureSpec.AT_MOST) {
        height = Math.min(desiredHeight, heightMeasureSpec);
    } else {
        height = desiredHeight;
    }

    setMeasuredDimension(width, height);
}

可以看到,视图的 widthheight 根据期望尺寸、强制尺寸和预定义的模式进行操作。最后调用 onMeasuredDimension() 方法来确定视图的尺寸。请注意,必须在方法结束时调用此方法以确定视图的尺寸。

值得一提的是,还有另一个名为 resovelSizeAndState() 的方法可以简化上述代码,如下面的代码块所示:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    int width = resolveSizeAndState(desiredWidth, widthMeasureSpec);
    int height= resolveSizeAndState(desiredHeight, heightMeasureSpec);

    setMeasuredDimension(width, height);
}

关于上面的代码,需要注意的是,必须考虑内边距距离才能正确调整大小。到目前为止,通过上述说明,测量方面已经完全清楚。

布局

此部分确定视图在屏幕上的确切位置,通过重写 onLayout() 方法进行评估。此方法有五个输入参数,分别是 changed(布尔值)、left、top、right 和 bottom(整数值)。通过布局,可以操纵视图的位置,使其精确地定位在最佳位置。第一个参数(changed)决定了此视图的包含布局的任何更改,它是一个布尔变量。

假设开发了一个围绕特定值中心的自定义视图。设 centerXcenterY 为自定义视图的位置。对于这种情况(如在开发的圆形进度条中使用),可以使用以下代码:

@Override
protected void onLayout(Boolean changed, int left, int top, int right, int bottom) {
    
   if(changed){
    centerX=(left+right)/2;
    centerY=(top+bottom)/2;
   }
}

很明显,自定义视图的中心位于布局的中心。

属性

自定义视图肯定包含一组样式和格式属性,这些属性必须准确实现。这些 XML 属性经过优化确定,以遵循自定义视图的配置。要定义这些 XML 属性,必须在 res/values/attrs.xml 中插入以下资源,如下所示。在此示例代码中,自定义视图的名称为 myCustomView,并考虑了其所需的属性。

<code><?xml version="1.0" encoding="utf-8"?>
<resources>
   <declare-styleable name="myCustumView">
       <attr name="backColor" format="Color />
       <attr name="textSize format="Integer" />
       ...
   </declare-styleable>
</resources></code>

上面的 XML 使我们能够定义一些所需的属性,如颜色、大小等。这些属性可以在静态和动态情况下进行修改。在静态情况下,可以通过在 layout XML 中设置其值来初始化这些属性的值,如下面的代码所示:

<?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"        
        tools:context="com.example.mycustomview.MainActivity">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:gravity="center_horizontal|center_vertical">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:layout_weight="1">

            <com.example.mycustomview
                android:layout_width="400dp"
                android:layout_height="400dp"
                android:id="@+id/pb1"
                app:backColor="#aaaaaa"                
                app:textSize="20"/>

        </LinearLayout>       
        </LinearLayout>
    </RelativeLayout>

事实上,可以强制预定义的属性具有上面代码中定义的某些初始值。

在动态情况下,自定义视图的属性通过本文附加的圆形进度条中所示的代码进行修改。

交互

标准的视图会对编程事件(如触摸、点击等)做出反应。对于每个可定义的事件,自定义视图都可以编程为适当交互,其中一些属性可能会发生变化。为此,在主 activity 中定义了一个自定义视图的实例,并实现了其事件监听器以执行某些特定任务。

例如,开发的圆形进度条被设计为响应单击事件。每次单击事件后,进度条的值会增加 5。此示例实现如下:

public class MainActivity extends AppCompatActivity {
    public ProgressBar progressBar1;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        progressBar1=(ProgressBar)findViewById(R.id.pb1);
        progressBar2=(ProgressBar)findViewById(R.id.pb2);
        progressBar1.setValue(80);
        progressBar2.setValue(30);
        progressBar1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (progressBar1.value<100) {
                    progressBar1.value+=5;
                    progressBar1.Invalidate();
                }else{
                    progressBar1.value=0;
                }
            }
        });       
    }
}

可以看到,进度条响应了单击事件,并且其值增加了。因此,必须精确修改在视觉上显示进度的量,并且视图应该被重绘(重新显示)。为此,调用了 Invalidate() 方法,该方法告知视图其某些属性已更改。

实际上,在这种情况下,可能需要重绘视图或其父级的属性。使用了一些方法来通知 Android 框架关于这种情况,这些方法列出如下,其目标是唤醒视图关于其无效化:

  • Invalidate():当需要重绘视图时调用此方法。它将导致 onDraw 立即被调用。必须在 UI 线程上调用此方法。
  • Postinvalidate():此方法与 Invalidate() 方法相同,不同之处在于它是在后台线程上调用的。
  • requestLayout():可能影响大小的更改应后跟调用此方法。此方法最终会触发 onMeasure()onLayout() 方法,不仅针对指定的视图,还针对父视图中的所有子视图。

根据发生的事件(更改)正确调用上述方法之一非常重要。这主要是因为这些方法具有不同的复杂性,在某些特殊情况下不需要进行评估。通常,在交互部分肯定会调用上述方法之一。

圆形进度条

基于定义的方面,通过以下代码开发了圆形进度条。首先,如下所示设置了视图的属性:

package com.example.mycustomview;

import android.content.Context;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.View;
import android.widget.Toast;

public class ProgressBar extends View {

    private int backColor=0;
    private int frontColor=0;
    private int lineColor=0;
    private boolean displayValue;
    private int outerCirlceRadius=200;
    private int innerCirlceRadius=150;
    private int displayTextSize=20;
    private int value=90;
    private int centerX=200;
    private int centerY=200;
    private int width=400;
    private int height=400;
    private String name="";

    public ProgressBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        setupAttrs(attrs);
    }
    private void setupAttrs(AttributeSet attrs){

        try {
            for(int i=0;i<attrs.getAttributeCount();i++) {
                if(attrs.getAttributeName(i).contains("backColor")) {
                    backColor = attrs.getAttributeIntValue(i, backColor);
                }else if(attrs.getAttributeName(i).contains("frontColor")){
                    frontColor = attrs.getAttributeIntValue(i, frontColor);
                }else if(attrs.getAttributeName(i).contains("lineColor")){
                    lineColor = attrs.getAttributeIntValue(i, lineColor);
                }else if(attrs.getAttributeName(i).contains("displayValue")){
                    displayValue = attrs.getAttributeBooleanValue(i, false);
                }else if(attrs.getAttributeName(i).contains("name")){
                    name = attrs.getAttributeValue(i);
                }else if(attrs.getAttributeName(i).contains("layout_width")) {
                    String text=attrs.getAttributeValue(i);
                           text=text.substring(0,text.length()-5);
                    width = Integer.parseInt(text);
                }else if(attrs.getAttributeName(i).contains("layout_height")) {
                    String text=attrs.getAttributeValue(i);
                           text=text.substring(0,text.length()-5);
                    height = Integer.parseInt(text);
                }
            }
        }catch(Exception ex) {
            Toast.makeText(getContext(),ex.getMessage(), Toast.LENGTH_SHORT).show();
        }
    }

    @Override
    protected void onLayout(boolean changed,int left,int top,int right,int bottom){
        if(changed){
            centerX=(left+right)/2;
            centerY=(top+bottom)/2;
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
        width = Math.min(width, widthMeasureSpec);
        height = Math.min(height, heightMeasureSpec);
        width=Math.min(width,height);
        height=Math.min(width,height);
        innerCirlceRadius=(35*width)/100;
        outerCirlceRadius=width/2;
        displayTextSize=width/10;
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onDraw(Canvas canvas){
        Paint paint=new Paint();
        paint.setColor(backColor);
        canvas.drawCircle(centerX,centerY,outerCirlceRadius,paint);
        paint.setColor(frontColor);
        RectF rectf=new RectF();
        rectf.left=centerX-outerCirlceRadius;rectf.top=centerY-outerCirlceRadius;
        rectf.right=centerX+outerCirlceRadius;
        rectf.bottom=centerY+outerCirlceRadius;
        if(value<=50) {
            canvas.drawArc(rectf, 180f, ((float)value)*3.6f, true, paint);
        }else{
            canvas.drawArc(rectf, 180f, 180f, true, paint);
            canvas.drawArc(rectf, 0f, (value-50)*3.6f, true, paint);
        }
        paint.setColor(Color.WHITE);
        canvas.drawCircle(centerX,centerY,innerCirlceRadius,paint);
        if(displayValue) {
            paint.setColor(lineColor);
            paint.setTextSize(displayTextSize);
            String displayText=name + " = "+ value;
            int len=displayText.length()*displayTextSize/4;
            canvas.drawText(displayText, centerX-len, centerY, paint);
        }
    }

    public void setValue(int value){
        this.value=value;
        invalidate();
    }
    public int getValue(){
        return this.value;
    }
}

圆形进度条的不同部分,如测量、布局和绘制,在上面的代码中得到了专门描述。然后,给出了主 activity 代码:

package com.example.mycustomview;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;

public class MainActivity extends AppCompatActivity {
    public ProgressBar progressBar1;
    public ProgressBar progressBar2;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        progressBar1=(ProgressBar)findViewById(R.id.pb1);
        progressBar2=(ProgressBar)findViewById(R.id.pb2);
        progressBar1.setValue(80);
        progressBar2.setValue(30);
        progressBar1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (progressBar1.getValue()<100) {
                    progressBar1.setValue(progressBar1.getValue() + 5);
                }else{
                    progressBar1.setValue(0);
                }
            }
        });
        progressBar2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (progressBar2.getValue()<100) {
                    progressBar2.setValue(progressBar2.getValue() + 5);
                }else{
                    progressBar2.setValue(0);
                }
            }
        });
    }
}

项目的主 activity 布局如下:

<?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:paddingBottom="@dimen/activity_vertical_margin"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        tools:context="com.example.mycustomview.MainActivity">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:gravity="center_horizontal|center_vertical"
            android:weightSum="2">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:layout_weight="1">

            <com.example.mycustomview.ProgressBar
                android:layout_width="400dp"
                android:layout_height="400dp"
                android:id="@+id/pb1"
                app:backColor="#aaaaaa"
                app:frontColor="#444444"
                app:lineColor="#000000"
                app:displayValue="true"
                app:name="PB1"/>

        </LinearLayout>
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:layout_weight="1">

            <com.example.mycustomview.ProgressBar
                android:layout_width="400dp"
                android:layout_height="400dp"
                android:id="@+id/pb2"
                app:backColor="#aa00aa"
                app:frontColor="#440044"
                app:lineColor="#000000"
                app:displayValue="true"
                app:name="PB2"/>

        </LinearLayout>
        </LinearLayout>
    </RelativeLayout>

最后,圆形进度条的 XML 属性如下所示:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ProgressBar">
        <attr name="backColor" format="color"/>
        <attr name="frontColor" format="color"/>
        <attr name="lineColor" format="color"/>
        <attr name="displayValue" format="boolean"/>
        <attr name="name" format="string"/>
    </declare-styleable>
</resources>

结果如图 2 所示

图 2:圆形进度条的两个已实现实例

开发的进度条可以轻松用于其他项目,这将在下一节中进行说明。

Using the Code

要使用开发的圆形进度条,只需将其插入到您的 XML 主 activity 布局中即可。

关注点

本文讨论了自定义视图的开发及其相关方面,并提供了一个圆形进度条以更好地理解上述概念。

历史

  • 2020年10月19日:初始版本
© . All rights reserved.