【自定义View】实现柱状图方法(一)


前言:自定义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.涂色画图方法(重点)

我选择了画两个圆和矩形来组成圆柱条,这里需要关注的点有几个。

  1. 找出圆和矩形拼接位置
  2. 在拼接的时候记得涂色需求
  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)

四、完整代码

  1. 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>
  1. 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)
    }
}
  1. 自定义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);
        }
    }
}
  1. 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,甚至加以扩展。当然我也会用第三方库来实现给大家看看。

在这里插入图片描述
如果有其他想法,请大佬们指点。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

薪火_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值