android RecyclerView实现柱状图
先上效果图:
思路
- 可以把每一个柱形图看作RecyclerView的单个item
- 横向的RecyclerView,而且每个item宽度是固定的
- item的高度,需要确定整个数据最大高度,然后按照比例来确定
实现
知识补充
不想看的可以直接跳过
setStackFromEnd
先讲关于RecyclerView的几个属性:layoutManager.setStackFromEnd(boolean)
和 layoutManager.setReverseLayout(boolean);
public void setStackFromEnd(boolean stackFromEnd) {
assertNotInLayoutOrScroll(null);
if (mStackFromEnd == stackFromEnd) {
return;
}
mStackFromEnd = stackFromEnd;
requestLayout();
}
setStackFromEnd()
是设置数据填充是否从底部开始的,mStackFromEnd
默认为flase。
setReverseLayout
public void setReverseLayout(boolean reverseLayout) {
assertNotInLayoutOrScroll(null);
if (reverseLayout == mReverseLayout) {
return;
}
mReverseLayout = reverseLayout;
requestLayout();
}
setReverseLayout
用于反转项目遍历和布局顺序。当设置为true时,会将第一项放置在ui的末尾类似,如此类推。mReverseLayout
默认为false
组合使用
setStackFromEnd(false) && setReverseLayout(false)
这两种就是默认情况,代码如下
recyclerView = (RecyclerView) findViewById(R.id.rv_bill);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(
this, LinearLayoutManager.HORIZONTAL, false);
linearLayoutManager.setStackFromEnd(false);
List<BillBean> billBeanArrayList = new ArrayList<>();
for (int i = 0; i < 12; i++) {
billBeanArrayList.add(new BillBean(i + ""));
}
BillListAdapter listAdapter = new BillListAdapter(this, billBeanArrayList);
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.setAdapter(listAdapter);
效果如下:正常的显示
setStackFromEnd(true) && setReverseLayout(false)
recyclerView = (RecyclerView) findViewById(R.id.rv_bill);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(
this, LinearLayoutManager.HORIZONTAL, false);
linearLayoutManager.setStackFromEnd(true);
List<BillBean> billBeanArrayList = new ArrayList<>();
for (int i = 0; i < 12; i++) {
billBeanArrayList.add(new BillBean(i + ""));
}
BillListAdapter listAdapter = new BillListAdapter(this, billBeanArrayList);
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.setAdapter(listAdapter);
效果:列表的排序顺序还是正常的,但是数据先从末端排列开始,所以会自动展示在末尾。
setStackFromEnd(false) && setReverseLayout(true)
recyclerView = (RecyclerView) findViewById(R.id.rv_bill);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(
this, LinearLayoutManager.HORIZONTAL, true);
linearLayoutManager.setStackFromEnd(false);
List<BillBean> billBeanArrayList = new ArrayList<>();
for (int i = 0; i < 12; i++) {
billBeanArrayList.add(new BillBean(i + ""));
}
BillListAdapter listAdapter = new BillListAdapter(this, billBeanArrayList);
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.setAdapter(listAdapter);
效果:倒过来排序,显示的是最开始的
setStackFromEnd(true) && setReverseLayout(true)
recyclerView = (RecyclerView) findViewById(R.id.rv_bill);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(
this, LinearLayoutManager.HORIZONTAL, true);
linearLayoutManager.setStackFromEnd(true);
List<BillBean> billBeanArrayList = new ArrayList<>();
for (int i = 0; i < 12; i++) {
billBeanArrayList.add(new BillBean(i + ""));
}
BillListAdapter listAdapter = new BillListAdapter(this, billBeanArrayList);
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.setAdapter(listAdapter);
效果如下:顺序倒排,但是显示栈底的
实现部分
recyclerView部分
这里账单展示样式肯定是最近月份的要优先展示,所以要反向设置mLayoutManager.setReverseLayout(true);
recyclerView = (RecyclerView) findViewById(R.id.rv_bill);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(
this, LinearLayoutManager.HORIZONTAL, true);
BillListAdapter listAdapter = new BillListAdapter(this, billBeanArrayList);
recyclerView.setLayoutManager(linearLayoutManager);
recyclerView.setAdapter(listAdapter);
item实现部分
看图可以分为上面4块,可以直接用LinearLayout
套住,或者自定义实现上面4块。这里采用自定义:
业务分析:先要考虑红色块的高度问题,金额最大和最低会可能相差很大的,这里最好获取列表最大值作为基准,每个金额去除以最大金额,获得自身高度的百分比。
业务显示数据类:根据要显示的内容,需要定义一个实体类来控制显示的内容。这里模拟为月账单业务,需要月份、最大金额(可自己先算好)、月金额、月份颜色、月金额颜色、柱状块颜色、线颜色(颜色也可以通过自定义属性设置)。
package com.hujin.wan;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import androidx.annotation.Nullable;
public class BarItemView extends View {
private Context mContext;
private Paint linePaint;
private int barColir;
private int lineColor;
private int moneyColor;
private int timeColor;
private Paint barPaint;
private Paint moneyPaint;
private Paint timePaint;
private int width;
private int heigt;
private int lineHeigt;
private int lineWidth;
private int barHeight;
private int maxBarHeight;
private float animValue;
private int animBarHeight;
private int pos;
public BarItemView(Context context) {
this(context, null);
}
public BarItemView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public BarItemView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
TypedArray typedArray = mContext.obtainStyledAttributes(attrs, R.styleable.BarItemView);
try {
barColir = typedArray.getColor(R.styleable.BarItemView_barColor, Color.RED);
lineColor = typedArray.getColor(R.styleable.BarItemView_lineColor, Color.GRAY);
moneyColor = typedArray.getColor(R.styleable.BarItemView_moneyColor, Color.GRAY);
timeColor = typedArray.getColor(R.styleable.BarItemView_textColor, Color.GRAY);
} finally {
typedArray.recycle();
}
init();
}
private BillBean billBean;
private boolean hasAnimation;
public void setValue(BillBean bean){
this.billBean = bean;
}
public synchronized void updateValue(int position, BillBean bean, boolean hasAnimation) {
this.pos = position;
this.billBean = bean;
this.hasAnimation=true;
if(hasAnimation) {
doAnimation();
}
postInvalidate();
}
ValueAnimator animator;
private void doAnimation() {
if (animator != null && animator.isRunning()) {
animator.cancel();
}
animator = ValueAnimator.ofFloat(0, 1);
animator.setDuration(1000);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
animValue = (float) animation.getAnimatedValue();
invalidate();
}
});
animator.start();
}
private void init() {
initLinePaint();
initBarPaint();
initTimePaint();
initMoneyPaint();
}
private void initMoneyPaint() {
moneyPaint = new Paint();
moneyPaint.setAntiAlias(true);
moneyPaint.setColor(moneyColor);
moneyPaint.setStyle(Paint.Style.FILL);
moneyPaint.setStrokeWidth(2);
}
private void initTimePaint() {
timePaint = new Paint();
timePaint.setAntiAlias(true);
timePaint.setColor(timeColor);
timePaint.setStyle(Paint.Style.FILL);
timePaint.setStrokeWidth(2);
}
private void initBarPaint() {
barPaint = new Paint();
barPaint.setAntiAlias(true);
barPaint.setColor(barColir);
barPaint.setStyle(Paint.Style.FILL_AND_STROKE);
}
private void initLinePaint() {
linePaint = new Paint();
linePaint.setAntiAlias(true);
linePaint.setColor(lineColor);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setStrokeWidth(2);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//宽度固定的,不需要测量了,高度由外表recyleview控制
heigt = getMeasuredHeight();
Log.e("距离1", "" + DisplayUtils.dip2px(60));
width = lineWidth = (int) DisplayUtils.dip2px(60);
//高度从上到下 ,下面越大
lineHeigt = heigt - DisplayUtils.dip2px(32);
// 宽度在外面限制了
//计算 bar的最大高度 == 总高 - (月份整体高度+金额文字高度+线的高度+margin距离 * 2)
Paint paint = new Paint();
Rect rect = new Rect();
paint.setTextSize(36);
paint.getTextBounds("测试", 0, 1, rect);
int textHeight = rect.height();
Log.e("距离3", "" + textHeight);
//bar最大高度 等于最大高度依次减去其他
maxBarHeight = heigt - DisplayUtils.dip2px(32 + textHeight + 6 * 2 + 1);
setMeasuredDimension(lineWidth, heigt);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//真正的宽高
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if(hasAnimation) {
animBarHeight = (int) (maxBarHeight * animValue);
}else {
animBarHeight = maxBarHeight;
}
drawLine(canvas);
drawTime(canvas);
drawBar(canvas);
drawMoney(canvas);
}
private int marginForMoney = DisplayUtils.dip2px(6);
private void drawMoney(Canvas canvas) {
canvas.save();
//动态的
int height = lineHeigt - barHeight;
Rect rect = new Rect();
String money = String.valueOf(billBean.countAmt);
timePaint.setTextSize(36);
timePaint.getTextBounds(money, 0, money.length(), rect);
//获得字符串的宽度
float strNameFix = timePaint.measureText(money) / 2;
canvas.drawText(money, width / 2 - rect.width() / 2, height - marginForMoney, timePaint);
}
private int barWidth = (int) DisplayUtils.dip2px(30);
private void drawBar(Canvas canvas) {
canvas.save();
int barStartWidth = width / 2 - barWidth / 2;
int barEndWidth = width / 2 + barWidth / 2;
//倒角
float arcRect = 15.0f;
//高度
// Log.e("animBarHeight ==",pos+" "+animBarHeight+""+" value=="+animValue);
barHeight = (int) (animBarHeight * billBean.percent);
//这里要判断
RectF oval1 = new RectF(barStartWidth,lineHeigt - barHeight + arcRect,barEndWidth,lineHeigt);
RectF oval2 = new RectF(barStartWidth+arcRect,lineHeigt - barHeight,barEndWidth-arcRect,lineHeigt);
RectF oval3 = new RectF(barStartWidth,lineHeigt -barHeight ,barStartWidth+2*arcRect,lineHeigt - barHeight+2*arcRect);
RectF oval4 = new RectF(barEndWidth - 2*arcRect,lineHeigt -barHeight,barEndWidth,lineHeigt - barHeight+2*arcRect);
canvas.drawRect(oval1, barPaint);
canvas.drawRect(oval2, barPaint);
canvas.drawArc(oval3,180,90,true,barPaint);
canvas.drawArc(oval4,270,90,true,barPaint);
}
/**
* 绘制 月份
*
* @param canvas
*/
private void drawTime(Canvas canvas) {
canvas.save();
Rect rect = new Rect();
String month = billBean.months;
timePaint.setTextSize(34);
timePaint.getTextBounds(month, 0, month.length(), rect);
//获得字符串的宽度
float strNameFix = timePaint.measureText(month) / 2;
canvas.drawText(month, width / 2 - rect.width() / 2, getHeight() - DisplayUtils.dip2px(16) + rect.height() / 2, timePaint);
}
private void drawLine(Canvas canvas) {
Path path = new Path();
path.moveTo(0, lineHeigt);
path.lineTo(lineWidth, lineHeigt);
canvas.drawPath(path, linePaint);
}
}
实体类(颜色再自定义属性里面)
public double countAmt;
public String months;
public boolean isAnim;
public float percent;
public BillBean(double countAmt, String months, boolean isAnim,double maxCount) {
this.countAmt = countAmt;
this.months = months;
this.isAnim = isAnim;
this.percent = (float) (countAmt/maxCount);
}
item布局
<com.hujin.wan.BarItemView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/biv_item"
app:lineColor="@color/colorAccent"
android:layout_width="60dp"
android:layout_height="match_parent"
android:background="@color/colorPrimary" />
假数据部分
List<BillBean> billBeanArrayList = new ArrayList<>();
for (int i = 1; i < 13; i++) {
billBeanArrayList.add(new BillBean(100*i,i+"月",true,12*100));
}