文章目录
前言:自定义VIew实现柱状图
通过自定义View实现柱状图的方法有很多种,因为最近在公司做需求用到了这部分知识,所以在这里用文章记录一下最简单的方法之一,我选择是去定义每个柱状条的View,然后通过RecyclerView的网格布局实现需求。
如图:
一、自定义柱形View
1.定义属性
主要属性有:初始颜色画笔,染色画笔,圆柱高,圆柱宽(宽度一般为柱体的原的直径),最大容量,真实容量等
private Paint startPaint;// 初始颜色
private Paint paint; // 定义颜色
private float height;// 圆柱高,记得上下腾出圆半径距离,即宽weight的二分之一
private float weight;// 宽
private float maxCapacity;// 最大容量
private float authenticCapacity;// 真实容量
2.初始化属性
因为这里UI已经严格要求了宽高,所以我在这里偷懒写死了宽高和画笔颜色,后续有空我会把它们做成自定义颜色扩展这个自定义View
// 定义宽高和容量
private void initMeasure(Context context) {
height = context.getResources().getDimension(R.dimen.base_dp_42);
weight = context.getResources().getDimension(R.dimen.base_dp_4);
maxCapacity = 0;
authenticCapacity = 0;
}
// 定义画笔
private void initPaint() {
startPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
startPaint.setColor(Color.parseColor("#EEEEEE"));
paint.setColor(Color.parseColor("#3BE0F8"));
}
3.涂色画图方法(重点)
我选择了画两个圆和矩形来组成圆柱条,这里需要关注的点有几个。
- 找出圆和矩形拼接位置
- 在拼接的时候记得涂色需求
- 在涂色时,需要注意坐标轴y轴向下为正,即在top的基础上增加比例容量的长度
private void onDrawRect(Canvas canvas) {
Log.d("TAG", "getWidth = : "+getWidth());
Log.d("TAG", "getHeight=: "+getHeight());
float centerX = getWidth() / 2f; // 求出X轴中心点
float centerY = getHeight() / 2f; // 求出Y轴中心点
float left = centerX - weight / 2f;
float right = centerX + weight / 2f;
float top = centerY - height / 2f;
float bottom = centerY + height / 2f;
// 绘制圆柱条的上面和下面两个圆
canvas.drawCircle(centerX, top, weight / 2f, startPaint); // 上面的圆
canvas.drawCircle(centerX, bottom, weight / 2f, startPaint); // 下面的圆
// 绘制圆柱条的侧面
canvas.drawRect(left, top, right, bottom, startPaint);
// 判断是否存在数据,选择涂色
if (maxCapacity!=0&&authenticCapacity!=0){
// 由于坐标系为反的,这里需要将比例求反
float v = 1f-(authenticCapacity / maxCapacity);
// 然后将比列去乘原来的柱长,再加上原来的top值。将绘制点下移,也就是y轴正方向
float topAuthentic = top+v*(bottom-top);
canvas.drawCircle(centerX, topAuthentic, weight / 2f, paint); // 上面的圆
canvas.drawCircle(centerX, bottom, weight / 2f, paint); // 下面的圆
canvas.drawRect(left, topAuthentic, right, bottom, paint);
}
}
4.暴露给外界的设置颜色和容量的方法
public void setCapacity(float maxCapacity,float authenticCapacity,int color) {
this.maxCapacity = maxCapacity;
this.authenticCapacity = authenticCapacity;
paint.setColor(color);
// 刷新
invalidate();
}
到这里,一个简单的圆柱条就完成了,如果不选择recyclerView的话,可以在这个View里添加边距属性和条数属性,根据外界设置的边距和条数进行绘制相同的View,同时可以嵌套的可滑动的View里增加滑动功能。但是因为项目急着上线,我这里便没有采取这种做法,后续如果有空再进行添加。
二、适配器Adapter,这里要关注的只有onBindViewHolder()方法
通过onBindViewHolder()对该自定义的暴露给外界的方法不断刷新View,给View进行染色和比例给予
@Override
public void onBindViewHolder(@NonNull RainPillarAdapter.ViewHolder holder, int position) {
if (position==0){
holder.rainPillarView.setCapacity(max,integers.get(position), Color.RED);
}else {
holder.rainPillarView.setCapacity(max,integers.get(position), Color.CYAN);
}
}
三、Activity活动页调用RecyclerView
val recyclerView = findViewById<RecyclerView>(R.id.rv_rain)
val integers: MutableList<Int> = ArrayList()
for (i in 1..39){
integers.add(Random().nextInt(40))
}
val rainPillarAdapter = RainPillarAdapter()
recyclerView.layoutManager = GridLayoutManager(this,integers.size)
recyclerView.adapter = rainPillarAdapter
rainPillarAdapter.setData(integers)
四、完整代码
- MainActivity.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:background="#C1B6B6">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="145dp"
app:layout_constraintTop_toTopOf="parent"
android:background="#ffffff"
android:layout_margin="12dp"
app:cardCornerRadius="15dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/iv_rain"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/rain"
android:layout_marginTop="12dp"
android:layout_marginStart="12dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="在未来两个小时的雨量"
android:textColor="#333333"
android:textSize="19dp"
android:layout_marginStart="12dp"
app:layout_constraintTop_toTopOf="@id/iv_rain"
app:layout_constraintStart_toEndOf="@id/iv_rain"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_rain"
android:layout_width="0dp"
android:layout_height="@dimen/base_dp_48"
android:layout_marginEnd="12dp"
android:layout_marginTop="12dp"
app:layout_constraintStart_toStartOf="@id/iv_rain"
app:layout_constraintTop_toBottomOf="@id/iv_rain"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</androidx.constraintlayout.widget.ConstraintLayout>
- MainActivity.kt
package com.itaem.blviewtest
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.noober.background.BackgroundLibrary
import java.util.*
import kotlin.collections.ArrayList
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
BackgroundLibrary.inject(this)
setContentView(R.layout.activity_main)
val recyclerView = findViewById<RecyclerView>(R.id.rv_rain)
val integers: MutableList<Int> = ArrayList()
for (i in 1..39){
integers.add(Random().nextInt(40))
}
val rainPillarAdapter = RainPillarAdapter()
recyclerView.layoutManager = GridLayoutManager(this,integers.size)
recyclerView.adapter = rainPillarAdapter
rainPillarAdapter.setData(integers)
}
}
- 自定义View:RainPillarView代码
public class RainPillarView extends View {
private Paint startPaint;// 初始颜色
private Paint paint; // 定义颜色
private float height;// 圆柱高,记得上下腾出圆半径距离,即宽weight的二分之一
private float weight;// 宽
private float maxCapacity;// 最大容量
private float authenticCapacity;// 真实容量
public RainPillarView(Context context) {
super(context);
}
public RainPillarView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initPaint();
initMeasure(context);
}
public RainPillarView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initPaint();
initMeasure(context);
}
private void initMeasure(Context context) {
height = context.getResources().getDimension(R.dimen.base_dp_42);
weight = context.getResources().getDimension(R.dimen.base_dp_4);
maxCapacity = 0;
authenticCapacity = 0;
}
private void initPaint() {
startPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
startPaint.setColor(Color.parseColor("#EEEEEE"));
paint.setColor(Color.parseColor("#3BE0F8"));
}
public void setCapacity(float maxCapacity,float authenticCapacity,int color) {
this.maxCapacity = maxCapacity;
this.authenticCapacity = authenticCapacity;
paint.setColor(color);
// 刷新
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 绘制矩形
onDrawRect(canvas);
}
private void onDrawRect(Canvas canvas) {
Log.d("TAG", "getWidth = : "+getWidth());
Log.d("TAG", "getHeight=: "+getHeight());
float centerX = getWidth() / 2f; // 求出X轴中心点
float centerY = getHeight() / 2f; // 求出Y轴中心点
float left = centerX - weight / 2f;
float right = centerX + weight / 2f;
float top = centerY - height / 2f;
float bottom = centerY + height / 2f;
// 绘制圆柱条的上面和下面两个圆
canvas.drawCircle(centerX, top, weight / 2f, startPaint); // 上面的圆
canvas.drawCircle(centerX, bottom, weight / 2f, startPaint); // 下面的圆
// 绘制圆柱条的侧面
canvas.drawRect(left, top, right, bottom, startPaint);
// 判断是否存在数据,选择涂色
if (maxCapacity!=0&&authenticCapacity!=0){
// 由于坐标系为反的,这里需要将比例求反
float v = 1f-(authenticCapacity / maxCapacity);
// 然后将比列去乘原来的柱长,再加上原来的top值。将绘制点下移,也就是y轴正方向
float topAuthentic = top+v*(bottom-top);
canvas.drawCircle(centerX, topAuthentic, weight / 2f, paint); // 上面的圆
canvas.drawCircle(centerX, bottom, weight / 2f, paint); // 下面的圆
canvas.drawRect(left, topAuthentic, right, bottom, paint);
}
}
}
- RainPillarAdapter代码
public class RainPillarAdapter extends RecyclerView.Adapter<RainPillarAdapter.ViewHolder> {
private List<Integer> integers = new ArrayList<>();
private Integer max;
public class ViewHolder extends RecyclerView.ViewHolder {
private RainPillarView rainPillarView;
public ViewHolder(@NonNull View itemView) {
super(itemView);
rainPillarView = itemView.findViewById(R.id.item_rain);
}
}
public void setData(List<Integer> integers){
this.integers = integers;
max = Collections.max(integers);
List<Integer> integers1 = new ArrayList<>();
integers1.add(1);
notifyDataSetChanged();
}
@NonNull
@Override
public RainPillarAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
return new ViewHolder(layoutInflater.inflate(R.layout.item_rain_pillar,parent,false));
}
@Override
public void onBindViewHolder(@NonNull RainPillarAdapter.ViewHolder holder, int position) {
if (position==0){
holder.rainPillarView.setCapacity(max,integers.get(position), Color.RED);
}else {
holder.rainPillarView.setCapacity(max,integers.get(position), Color.CYAN);
}
}
@Override
public int getItemCount() {
return integers.size();
}
}
总结
实现如上需求的方法是在数不胜数,而且其实也有优秀的第三方库借助实现,而且他的UI也很好看,不过因为这个公司项目,所以在UI方法被限制死了,加上时间比较匆忙,所以只能自己着手绘制一个简单的自定义View在加上rv实现需求。
当然后续如果有时间,我也会完善这个View,甚至加以扩展。当然我也会用第三方库来实现给大家看看。
如果有其他想法,请大佬们指点。