Android 自定义带动画的柱状图

android · 浏览次数 : 9

小编点评

本文介绍了一种简单的方法来制作柱状图,适用于展示用户一周的数据。通过使用自定义的ProgressBar组件和自定义的容器组件,实现了UI样式、动画自定义以及自由扩展等功能。 1. 使用三方SDK 通常情况下,使用三方SDK(如MPAndroidChart、ECharts等)会面临体积较大、定制性差和对特定UI需求的兼容性差等问题。 2. 自己绘制柱状图 自行绘制柱状图虽然可以解决上述问题,但过程较为复杂,需要考虑各种兼容性和适配问题。 3. 自定义柱状图 本文提出了一种简单的方式来制作柱状图,包括自定义progressDrawable以实现纵向线条绘制,并提供了两种纵向线条绘制drawable供选择。同时,还编写了一个自定义柱子组件,用于展示文本和添加动画等。 4. 自定义容器组件 为了管理柱状图对象,本文提供了一个自定义容器组件(如WeekView),用于展示多个柱状图。该组件支持动态创建和加载数据条对象。 5. 如何使用 在页面布局中,只需使用自定义的WeekView组件和Button按钮即可。通过设置数据和点击事件,即可实现柱状图的展示和数据刷新。 总之,本文提供了一种简单、灵活且易于扩展的方法来实现柱状图的功能,适用于展示用户一周的数据。

正文

功能分析

假设要使用柱状图展示用户一周的数据,通用的做法是对接三方图表SDK或者自己通过代码绘制。

1、三方SDK通常包体较大,且定制性差,对特定的UI需求兼容性差;
2、自己绘制,比较复杂,而且要考虑各种兼容适配;

今天,我们使用一种简单的方式,来制作柱状图,不仅代码简单,而且支持UI样式、动画自定义,更难得的是可以自由扩展 😁

如何实现?

另辟蹊径。

统计图表里,无非就是一个个表示数据的柱子而已。根据数值的大小,展示不同的高度柱子即可。

我们可使用ProgressBar组件表示柱子,其progress值对应实际的数值大小;
然后根据真实数据条数,创建对应数量的ProgressBar组件,加入到容器组件中,就可以实现柱状图了。

1. 自定义柱子

ProgressBar通常只有横向线条、圆圈样式,没有垂直的样式。
查看其样式源码,不难发现,progressDrawable是用来绘制进度条的,其实现是个layer-list

<style name="Widget.ProgressBar.Horizontal">
        <item name="indeterminateOnly">false</item>
        <item name="progressDrawable">@drawable/progress_horizontal</item>
        <item name="indeterminateDrawable">@drawable/progress_indeterminate_horizontal</item>
        <item name="minHeight">20dip</item>
        <item name="maxHeight">20dip</item>
        <item name="mirrorForRtl">true</item>
</style>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@android:id/background">
    ...

据此,我们完全可自定义progressDrawable,来实现纵向线条绘制,里面也可任意定义线条的颜色等样式属性。

下面,我们制作两种纵向线条绘制drawable:

progress_vertical_shade_drawable.xml 【案例中深色线条样式,用于表示数值较大的线条效果。请在顶部gif图上查看】

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@android:id/background">
        <shape>
            <solid android:color="#00E3DBF0" />
            <corners
                android:bottomLeftRadius="0dp"
                android:bottomRightRadius="0dp"
                android:topLeftRadius="0dp"
                android:topRightRadius="0dp" />
        </shape>
    </item>
    <item android:id="@android:id/progress">
        <scale
            android:scaleWidth="0%"
            android:scaleHeight="100%"
            android:scaleGravity="bottom">
            <shape>
                <!--这里是设置填充颜色和方向-->
                <gradient
                    android:angle="270"
                    android:endColor="#D2D1E6"
                    android:centerColor="#C0B3EA"
                    android:startColor="#C0B3EA"
                    android:type="linear" />
                <corners
                    android:bottomLeftRadius="8dp"
                    android:bottomRightRadius="8dp"
                    android:topLeftRadius="8dp"
                    android:topRightRadius="8dp" />
            </shape>
        </scale>
    </item>
</layer-list>

progress_vertical_tint_drawable.xml【案例中浅色线条样式,用于表示数值较小的线条效果。请在顶部gif图上查看】

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@android:id/background">
        <shape>
            <solid android:color="#00E3DBF0" />
            <corners
                android:bottomLeftRadius="0dp"
                android:bottomRightRadius="0dp"
                android:topLeftRadius="0dp"
                android:topRightRadius="0dp" />
        </shape>
    </item>
    <item android:id="@android:id/progress">
        <scale
            android:scaleWidth="0%"
            android:scaleHeight="100%"
            android:scaleGravity="bottom">
            <shape>
                <!--这里是设置填充颜色和方向-->
                <gradient
                    android:angle="270"
                    android:endColor="#E3DBF0"
                    android:centerColor="#E3DBF0"
                    android:startColor="#E3DBF0"
                    android:type="linear" />
                <corners
                    android:bottomLeftRadius="8dp"
                    android:bottomRightRadius="8dp"
                    android:topLeftRadius="8dp"
                    android:topRightRadius="8dp" />
            </shape>
        </scale>
    </item>
</layer-list>

我们找个布局测试一下:

堪称完美。

2. 自定义柱子组件

写过RecyclerView的大佬们,都知道列表item要单独定义出来的意义。

我们的柱子,不仅要展示颜色条,还要展示文本,添加动画、绑定数据等。
所以,我们单独写一个柱子组件,来做这些事情

DayView.java

package com.qxc.muyu.main.view;

import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.animation.LinearInterpolator;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;

import androidx.core.content.ContextCompat;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;

import com.qxc.muyu.R;

public class DayView extends RelativeLayout {
    TextView tv_title;
    TextView tv_text;
    ProgressBar pb;

    public DayView(Context context) {
        super(context);
        initView(context);
    }

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

    void initView(Context context) {
        View view = LayoutInflater.from(context).inflate(R.layout.view_statis_day, this);
        tv_title = view.findViewById(R.id.tv_title);
        tv_text = view.findViewById(R.id.tv_text);
        pb = view.findViewById(R.id.pb);
    }

    public void setData(String title, String text, int maxProgress, int progress, int styleProgressBar, boolean hasAnim) {
        tv_title.setText(title);
        tv_text.setText(text);
        pb.setMax(maxProgress);

        int drawableId = styleProgressBar == 1 ? R.drawable.progress_vertical_shade_drawable : R.drawable.progress_vertical_tint_drawable;
        Drawable customDrawable = ContextCompat.getDrawable(getContext(), drawableId);
        pb.setProgressDrawable(customDrawable);
        if (hasAnim) {
            startAnim(0, progress, 500);
        } else {
            pb.setProgress(progress);
        }
    }

    public void startAnim(int from, int to, int duration) {
        ObjectAnimator alphaTitle = ObjectAnimator.ofFloat(tv_title, "alpha", 0, 1);
        alphaTitle.setInterpolator(new LinearInterpolator());

        ObjectAnimator alphaText = ObjectAnimator.ofFloat(tv_text, "alpha", 0, 1);
        alphaText.setInterpolator(new LinearInterpolator());

        ValueAnimator animProgress = ValueAnimator.ofFloat(from, to);
        animProgress.setInterpolator(new FastOutSlowInInterpolator());
        animProgress.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                try {
                    float animatedValue = (float) animation.getAnimatedValue();
                    pb.setProgress((int) animatedValue);
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        });

        AnimatorSet set = new AnimatorSet();
        set.play(alphaTitle).with(alphaText).with(animProgress);
        set.setDuration(duration);
        set.start();
    }
}

其布局文件:
view_statis_day.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_weight="1"
    android:layout_marginLeft="5dp"
    android:layout_marginRight="5dp"
    android:background="@drawable/shape_week_bg">

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:text=""
        android:layout_marginBottom="10dp"
        android:letterSpacing="0.05"
        android:textColor="@color/colorBlack"
        android:textSize="11sp" />

    <ProgressBar
        android:id="@+id/pb"
        style="@android:style/Widget.ProgressBar.Horizontal"
        android:layout_width="50dp"
        android:layout_height="match_parent"
        android:layout_above="@id/tv_title"
        android:layout_centerInParent="true"
        android:layout_marginBottom="10dp"
        android:layout_marginTop="10dp"
        android:max="100"
        android:progress="50"
        android:progressDrawable="@drawable/progress_vertical_shade_drawable" />

    <TextView
        android:id="@+id/tv_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBottom="@id/pb"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="10dp"
        android:letterSpacing="0.05"
        android:text=""
        android:textColor="@color/colorBlack"
        android:textSize="12sp"/>
</RelativeLayout>

3. 自定义容器组件

有了一个个柱子,我们的柱子对象是不是需要管理起来,我们就需要一个容器,来放置这些柱子。

假设,我们有一周的数据,展示7个柱子就可以了,使用LinearLayout作为容器就行;
假设,我们要展示一个月的数据,使用RecyclerView、SrcollView作为容器都可以,因为都支持滑动;
更多的场景,大佬们请自个思考吧,怕想多了,伤我脑仁

如题,本案例中我们选择LinearLayout作为容器。
实现逻辑:

  1. 接收外界数据
  2. 遍历数据,动态创建、添加柱子组件

WeekView.java

package com.qxc.muyu.main.view;

import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;

import com.qxc.muyu.R;

import java.util.Collections;
import java.util.List;

public class WeekView extends RelativeLayout {
    LinearLayout ll_week;
    boolean hasAnim = true;

    public WeekView(Context context) {
        super(context);
        initView(context);
    }

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

    void initView(Context context) {
        View view = LayoutInflater.from(context).inflate(R.layout.view_statis_week, this);
        ll_week = view.findViewById(R.id.ll_week);
    }

    //接收外界数据,动态创建 & 加载数据条对象
    public void setData(List<String> titles, List<Integer> numbers) {
        if (titles == null || numbers == null || numbers.size() == 0 || titles.size() != numbers.size()) {
            return;
        }
        ll_week.removeAllViews();
        int max = Collections.max(numbers);
        for (int i = 0; i < numbers.size(); i++) {
            String title = titles.get(i);
            int num = numbers.get(i);
            String text = formatNumber(num);
            DayView dayView = new DayView(getContext());
            int styleProgressBar = max / 2 > num ? 2 : 1;
            dayView.setData(title, text, max, num, styleProgressBar, hasAnim);
            LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0, LayoutParams.MATCH_PARENT, 1);
            ll_week.addView(dayView,params);
        }
    }

    private String formatNumber(int num) {
        if (num < 1000) {
            return String.valueOf(num);
        } else if (num < 10000) {
            double value = num / 1000.0;
            return String.format("%.2fk", value);
        } else if (num < 100000000) {
            double value = num / 10000.0;
            return String.format("%.2f万", value);
        } else {
            double value = num / 100000000.0;
            return String.format("%.2f亿", value);
        }
    }
}

其布局文件(只有一个容器,简单的都没法说)
view_statis_week.xml

<?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:orientation="horizontal"
    android:paddingLeft="5dp"
    android:paddingRight="5dp"
    android:id="@+id/ll_week"
    android:background="@drawable/shape_week_bg">

</LinearLayout>

至此,周数据柱形图表功能已写完了。

4. 如何使用

在页面布局中,使用我们的自定义组件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ffffff"
    android:paddingLeft="15dp"
    android:paddingTop="50dp"
    android:paddingRight="15dp">

    <com.qxc.muyu.main.view.WeekView
        android:id="@+id/week"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        android:layout_centerInParent="true" />

    <Button
        android:id="@+id/btn"
        android:layout_width="200dp"
        android:layout_height="50dp"
        android:layout_below="@id/week"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="50dp"
        android:background="#00000000"
        android:text="刷新数据" />

</RelativeLayout>

代码中,给自定义组件设置数据:

WeekView weekView = view.findViewById(R.id.week);
        Button btn = view.findViewById(R.id.btn);
        List<String> titles = new ArrayList<>();
        titles.add("周一");
        titles.add("周二");
        titles.add("周三");
        titles.add("周四");
        titles.add("周五");
        titles.add("周六");
        titles.add("周日");
        List<Integer> numbers = new ArrayList<>();
        numbers.add(1200);
        numbers.add(800);
        numbers.add(500);
        numbers.add(400);
        numbers.add(2200);
        numbers.add(2000);
        numbers.add(888);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                weekView.setData(titles, numbers);
            }
        });

就是这么简单,UI样式想怎么调都行,好用到飞起,简直了,哈哈~

与Android 自定义带动画的柱状图相似的内容:

Android 自定义带动画的柱状图

功能分析 假设要使用柱状图展示用户一周的数据,通用的做法是对接三方图表SDK或者自己通过代码绘制。 1、三方SDK通常包体较大,且定制性差,对特定的UI需求兼容性差; 2、自己绘制,比较复杂,而且要考虑各种兼容适配; 今天,我们使用一种简单的方式,来制作柱状图,不仅代码简单,而且支持UI样式、动画自

Android无障碍自动化结合opencv实现支付宝能量自动收集

Android无障碍服务可以操作元素,手势模拟,实现基本的控制。opencv可以进行图像识别。两者结合在一起即可实现支付宝能量自动收集。opencv用于识别能量,无障碍服务用于模拟手势,即点击能量。 当然这两者结合不单单只能实现这些,还能做很多自动化的程序,如芭芭农场自动施肥、蚂蚁庄园等等的自动化,

[Android 逆向]绕过小米需插卡安装apk限制

1. 确保自己手机是root的了 2. 给手机安装busybox,使可以用vi编辑文件 安装方法: 0. adb shell getprop ro.product.cpu.abi 获得 cpu架构信息 arm64-v8a 1. 下载 https://busybox.net/downloads/bin

【Android逆向】制作Youpk脱壳机,完成对NCSearch的脱壳操作

1. 拉去youpk 代码或镜像,自行编译构建 youpk 代码地址 https://github.com/youlor/unpacker 2. 执行 adb reboot bootloader 3. 执行 sh flash-all.sh 4. 安装NCSearch,并启动app 5. 执行adb

android 逆向笔记

壳检测工具 GDA 2. 逆向分析APP 一般流程 1. 使用自动化检测工具检测APP是否加壳,或者借助一些反编译工具依靠经验判断是否加壳 2. 如果apk加壳,则需要先对apk进行脱壳 3. 使用`jeb`, `jadx`, `apktool`等反编译工具对apk进行反编译 4. 先依据静态分析得

Android 架构模式如何选择

Android架构模式飞速演进,目前已经有MVC、MVP、MVVM、MVI。到底哪一个才是自己业务场景最需要的,不深入理解的话是无法进行选择的。这篇文章就针对这些架构模式逐一解读。重点会介绍Compose为什么要结合MVI进行使用。希望知其然,然后找到适合自己业务的架构模式。

3种方式自动化控制APP

自动化控制APP不管是在工作还是生活方面,都可以帮助我们高效地完成任务,节省时间和精力。本文主要介绍自动化控制APP的3种常用方式。 1、Python + adb 这种方式需要对Android有一些基本的了解。adb是一种用于调试Android应用程序的工具。使用Python和adb可以轻松实现自动

使用 shell 脚本自动申请进京证 (六环外) —— debug 过程

写好的自动办理六环外进京证脚本跑不通,总是返回办理业务人数较多 (500) 错误,Charles / VNET 抓包、android 交叉编译 jq、升级 curl…都不起作用,最终还是神奇的 adb shell 帮了大忙,最后定位到根因,居然是用 shell 字符串长度作为数据长度导致的,这错误犯的有点低级……

【AppStore】一文让你学会IOS应用上架Appstore

咱们国内现在手机分为两类,Android手机与苹果手机,现在用的各类APP,为了手机的使用安全,避免下载到病毒软件,官方都极力推荐使用手机自带的应用商城进行下载,但是国内Android手机品类众多,手机商城各式各样,做不到统一,所以Android的APP上架得一个一个平台去申请上架,一直让开发人员头...

厦门狄耐克:助推智慧医疗,需要夯实自身的技术底座

摘要:在推动医疗信息化发展的进程中,厦门狄耐克联合华为云DTSE团队,共同推出了智慧医护空间解决方案,将原有的Android系统替换成Open Harmony,打造了基于开源鸿蒙统一技术底座的智慧医院生态。 本文分享自华为云社区《华为云DTSE团队联合厦门狄耐克打造智慧医护空间解决方案》,作者:华为