Android 自定义带动画的柱状图
功能分析
假设要使用柱状图展示用户一周的数据,通用的做法是对接三方图表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作为容器。
实现逻辑:
- 接收外界数据
- 遍历数据,动态创建、添加柱子组件
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 自定义带动画的柱状图的更多相关文章
- 自定义带动画的Toast
一.style样式: 1. // 移动和透明渐变结合的动画 <style name="anim_view"> <item name="@ ...
- Android 自定义波浪动画 --"让进度浪起来~"
原文链接:http://www.jianshu.com/p/0e25a10cb9f5 一款效果不错的动画,实现也挺简单的,推荐阅读学习~ -- 由 傻小孩b 分享 waveview <Andro ...
- Android 自定义帧动画
Android 自定义帧动画 Android L : Android Studio 帧动画 和gif图片类似,顺序播放准本好的图片文件:图片资源在xml文件中配置好 将图片按照预定的顺序一张张切换,即 ...
- Android 自定义带刻度的seekbar
自定义带刻度的seekbar 1.布局 <span style="font-family:SimHei;font-size:18px;"><com.imibaby ...
- Android自定义窗口动画
第一步,设置出现和消失的xml 1.在res/anim下创建enter_anim.xml,设置窗口出现的动画 <?xml version="1.0" encoding=&qu ...
- 实例源码--Android自定义Gallery动画效果
相关文档与源码: 下载源码 技术要点: 1.自定义控件的使用 2.Gallery控件的使用实例 3.详细的源码注释 ...... 详细介绍: 1.自定义控件的使用 本套源码通过自定义控件的方式,继 ...
- Android自定义带标题边框的Layout
今天工作中又碰到个小问题,项目需要用到像Java Swing的JPanel一样带标题边框的布局,Android里没有类似控件,想到这个也不难,自己画了一个,是继承LinearLayout的一个自定义布 ...
- Android 自定义PopupWindow动画效果
public class RollActivity extends Activity { private View view; private Button btn; private PopupWin ...
- Android -- 自定义带进度条的按钮
1. 实现了一个带进度条的按钮,完成后显示提示信息,并设置按钮为不可再次被点击
- Android 自定义带回调的Dialog 及EditText相关
import android.app.Activity; import android.content.Context; import android.text.Editable; import ...
随机推荐
- 提示工程(Prompt Engineering)将ChatGPT调教为傲娇猫娘~喵
Prompt Engineering(提示工程)是指通过设计精心构造的提示(prompt)或者输入,来引导大型语言模型生成特定类型的输出.这个技术背后的原理是利用模型对输入的敏感性,通过提供特定格式或 ...
- 纯css高斯背景模糊(毛玻璃,伪元素,完整实例)
先上效果图: 写博客不管是做笔记还是干啥,直接上源码不行么,还不放效果图,拆分成几段谁慢慢看,慢慢理解去 自己动手,丰衣足食,上代码: <!DOCTYPE HTML> <html l ...
- vue项目(cli-3)替换浏览器logo
思路: 根html文件的 <link rel="icon" href="<%= BASE_URL %>favicon.ico">是引入i ...
- hbuilder打包报错:java.lang.IllegalStateException: Expected BEGIN_OBJECT but was BEGIN_ARRAY at line 7 column 15 path $.icons
一个棘手的问题,在网上找几乎没有出现这样的案例,个别也只有翻译没有解决方式,,,,,自己研究一番发现这实际上都不算是个问题 这句话翻译:这个位置应该是个对象而不是数组,解决方法: 在manifest. ...
- 生成带重复的笛卡尔乘积过程 Cartesian Product with Repetition
目录 What is Cartesian Product with Repetition Code Demo What is Cartesian Product with Repetition 比如说 ...
- 简说Python之列表,元祖,字典
目录 Python列表 创建列表 添加元素 查询元素 列表分片 分片简写 修改元素 一些其他添加列表元素的方法 extend() insert() 删除元素 remove()删除 del 通过索引删除 ...
- 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自带客户端命令 ...
- 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 ...
- 原生微信小程序
new Date 跨平台兼容性问题 在 Andriod 使用 new Date("2018-05-30 00:00:00")木有问题,但是在ios 下面识别不出来.因为 IOS 下 ...
- v-for比v-if优先级更高
在V2当中,v-for的优先级更高,而在V3当中,则是v-if的优先级更高. 在V3当中,做了v-if的提升优化,去除了没有必要的计算, 但同时也会带来一个无法取到v-for当中遍历的item问题, ...