功能分析

假设要使用柱状图展示用户一周的数据,通用的做法是对接三方图表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
```js
<?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 自定义带动画的柱状图的更多相关文章

  1. 自定义带动画的Toast

    一.style样式: 1.  // 移动和透明渐变结合的动画 <style name="anim_view">        <item name="@ ...

  2. Android 自定义波浪动画 --"让进度浪起来~"

    原文链接:http://www.jianshu.com/p/0e25a10cb9f5 一款效果不错的动画,实现也挺简单的,推荐阅读学习~ -- 由 傻小孩b 分享 waveview <Andro ...

  3. Android 自定义帧动画

    Android 自定义帧动画 Android L : Android Studio 帧动画 和gif图片类似,顺序播放准本好的图片文件:图片资源在xml文件中配置好 将图片按照预定的顺序一张张切换,即 ...

  4. Android 自定义带刻度的seekbar

    自定义带刻度的seekbar 1.布局 <span style="font-family:SimHei;font-size:18px;"><com.imibaby ...

  5. Android自定义窗口动画

    第一步,设置出现和消失的xml 1.在res/anim下创建enter_anim.xml,设置窗口出现的动画 <?xml version="1.0" encoding=&qu ...

  6. 实例源码--Android自定义Gallery动画效果

    相关文档与源码: 下载源码   技术要点: 1.自定义控件的使用 2.Gallery控件的使用实例 3.详细的源码注释 ...... 详细介绍: 1.自定义控件的使用 本套源码通过自定义控件的方式,继 ...

  7. Android自定义带标题边框的Layout

    今天工作中又碰到个小问题,项目需要用到像Java Swing的JPanel一样带标题边框的布局,Android里没有类似控件,想到这个也不难,自己画了一个,是继承LinearLayout的一个自定义布 ...

  8. Android 自定义PopupWindow动画效果

    public class RollActivity extends Activity { private View view; private Button btn; private PopupWin ...

  9. Android -- 自定义带进度条的按钮

    1. 实现了一个带进度条的按钮,完成后显示提示信息,并设置按钮为不可再次被点击

  10. Android 自定义带回调的Dialog 及EditText相关

      import android.app.Activity; import android.content.Context; import android.text.Editable; import ...

随机推荐

  1. 提示工程(Prompt Engineering)将ChatGPT调教为傲娇猫娘~喵

    Prompt Engineering(提示工程)是指通过设计精心构造的提示(prompt)或者输入,来引导大型语言模型生成特定类型的输出.这个技术背后的原理是利用模型对输入的敏感性,通过提供特定格式或 ...

  2. 纯css高斯背景模糊(毛玻璃,伪元素,完整实例)

    先上效果图: 写博客不管是做笔记还是干啥,直接上源码不行么,还不放效果图,拆分成几段谁慢慢看,慢慢理解去 自己动手,丰衣足食,上代码: <!DOCTYPE HTML> <html l ...

  3. vue项目(cli-3)替换浏览器logo

    思路: 根html文件的 <link rel="icon" href="<%= BASE_URL %>favicon.ico">是引入i ...

  4. hbuilder打包报错:java.lang.IllegalStateException: Expected BEGIN_OBJECT but was BEGIN_ARRAY at line 7 column 15 path $.icons

    一个棘手的问题,在网上找几乎没有出现这样的案例,个别也只有翻译没有解决方式,,,,,自己研究一番发现这实际上都不算是个问题 这句话翻译:这个位置应该是个对象而不是数组,解决方法: 在manifest. ...

  5. 生成带重复的笛卡尔乘积过程 Cartesian Product with Repetition

    目录 What is Cartesian Product with Repetition Code Demo What is Cartesian Product with Repetition 比如说 ...

  6. 简说Python之列表,元祖,字典

    目录 Python列表 创建列表 添加元素 查询元素 列表分片 分片简写 修改元素 一些其他添加列表元素的方法 extend() insert() 删除元素 remove()删除 del 通过索引删除 ...

  7. DNS(3) -- dns常用命令-rndc-dig-host-nslookup

    目录 1 bind自带客户端命令 1.1 rndc命令 1.2 检查配置文件语法 2 客户端测试命令 2.1 dig命令 2.2 host命令 2.3 nslookup命令 1 bind自带客户端命令 ...

  8. WEB服务与NGINX(20)- nginx 实现HTTP反向代理功能

    目录 1. nginx实现反向代理功能 1.1 nginx代理功能概述 1.2 NGINX实现HTTP反向代理 1.2.1 HTTP反向代理基本功能 1.2.1.1 反向代理配置参数 1.2.1.2 ...

  9. 原生微信小程序

    new Date 跨平台兼容性问题 在 Andriod 使用 new Date("2018-05-30 00:00:00")木有问题,但是在ios 下面识别不出来.因为 IOS 下 ...

  10. v-for比v-if优先级更高

    在V2当中,v-for的优先级更高,而在V3当中,则是v-if的优先级更高. 在V3当中,做了v-if的提升优化,去除了没有必要的计算, 但同时也会带来一个无法取到v-for当中遍历的item问题, ...