Android自定义柱状图控件

搜索github会发现已经有很多相对成熟的图表框架,但与我们拿到的ui效果图总会有一些差别,然后就会陷入是把别人源码下载下来修改之后作为已用,或是重新定义一个新控件的两难境地。初拿到效果图感觉还是比较复杂难以实现,但是仔细观察发现其实这就是一个列表,一个横向列表,一个可以用RecyclerView实现的列表。


效果图.gif

如果借助recyclerview的话,那么重点就在每个条目也就是柱状图每个柱子的实现。

public class BarGraphItem extends View {
    private static final String TAG = "BarGraphView";
    private Paint paint;
    private int measuredWidth;
    private int measuredHeight;
    private double ratio;
    private GradientDrawable gradientDrawable;

    public BarGraphItem(Context context) {
        super(context);
        initPaint();
    }

    public BarGraphItem(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initPaint();
    }

    public BarGraphItem(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initPaint();
    }

    //设置柱子的高度
    public void setRatio(double ratio) {
        this.ratio = ratio;
        invalidate();
    }

    private void initPaint() {
        paint = new Paint();
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(getResources().getColor(R.color.colorBarBack));
        paint.setAntiAlias(true);

        int colors[] = {getResources().getColor(R.color.colorBarColor), getResources().getColor(R.color.colorGradientGreen)};
        gradientDrawable = new GradientDrawable(GradientDrawable.Orientation.BOTTOM_TOP,colors);
        //设置渐变色
        gradientDrawable.setGradientType(GradientDrawable.LINEAR_GRADIENT);
        //设置顶部圆角
        gradientDrawable.setCornerRadii(new float[]{15,15,15,15,0,0,0,0});
    }
     

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        measuredWidth = getMeasuredWidth();
        measuredHeight = getMeasuredHeight();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //画柱子的背景色
        canvas.drawRect(0, 0, measuredWidth, measuredHeight, paint);
        //计算柱子实际高度
        if (ratio != 0){
            int ratioHeight = (int) (measuredHeight * ratio + 0.5);
            //默认坐标原点在左上角,这里我们把画布移到左上角
            canvas.translate(0,measuredHeight - ratioHeight);
            //x,y,w,h
            gradientDrawable.setBounds(0,0,measuredWidth, ratioHeight);
            gradientDrawable.draw(canvas);
        }
    }
}

代码并不复杂,可以看到最终效果有很多渐变色的运用,平时我们会用shape标签绘制一些简单的图形,其中有一个gradient属性可以帮我们实现渐变效果,那在代码中可以直接使用GradientDrawable来实现同样的效果。接下来在布局文件中使用

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    android:orientation="vertical"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:gravity="center_horizontal"
    android:paddingTop="4dp"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">
    <TextView
        android:textSize="9dp"
        android:text="--"
        android:id="@+id/tv_amount_bar_graph"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />
    <RelativeLayout
        android:id="@+id/rl_bar_graph"
        android:layout_marginTop="8dp"
        android:background="@drawable/shape_bar_graph"
        android:layout_width="43dp"
        android:layout_height="wrap_content">
        <com.example.a14617.bargraphtest.view.BarGraphItem
            android:layout_centerHorizontal="true"
            android:id="@+id/bgi_bar_graph"
            android:layout_gravity="center_horizontal"
            android:layout_width="8dp"
            android:layout_height="110dp" />
    </RelativeLayout>
    <TextView
        android:textColor="#999999"
        android:textSize="12dp"
        android:gravity="center"
        android:text="--"
        android:background="#F9F9F9"
        android:layout_marginTop="8dp"
        android:id="@+id/tv_time_bar_graph"
        android:layout_width="match_parent"
        android:layout_height="40dp" />
    <ImageView
        android:id="@+id/iv_highest_bar_graph"
        android:visibility="invisible"
        android:src="@drawable/triangle_red_rose"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

</LinearLayout>

因为柱状图整体的渐变背景并没到底部,只是中间柱子的部分,所以把这个背景的设置放到item的布局中,也是用shape 标签实现

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <gradient
        android:angle="90"
        android:startColor="@color/colorLightBlue"
        android:endColor="@color/colorGradientWhite"
        >
    </gradient>
</shape>

标识最高点的红色三角可以用png或其它格式的图片,为了尽量缩减包体积也可以用layerlist标签来实现,

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/shape_id">
         <!--正三角-->
        <rotate
            android:fromDegrees="45"
            android:toDegrees="45"
            android:pivotX="-40%"
            android:pivotY="80%">
            <shape android:shape="rectangle">
                <solid android:color="@color/colorAccent"/>
                <size android:width="8dp"
                    android:height="8dp"/>
            </shape>
        </rotate>
    </item>
</layer-list>

准备工作已经完成,接下来就可以愉快地进行recyclerview三步曲了。在xml布局文件中使用recyclerview(比较简单代码就省略了),然后在MainActivity中初始化recyclerview,

private void initRecyclerView() {
        //先造几条假数据
        DecimalFormat df = new DecimalFormat("0.00");
        int maxIndex = 0;
        double max = 0;
        for (int i = 1;i <= 10;i++){
            IncomeDetailsBean incomeDetailsBean = new IncomeDetailsBean();
            incomeDetailsBean.setDate(i + "点");
            double random = Math.random();
            //记录最大值
            if (random > max) {
                max = random;
                maxIndex = i - 1;
            }
            incomeDetailsBean.setIncome(df.format(random * 10) + "");
            incomeDetailsBean.setRatio(random);
            incomeDetails.add(incomeDetailsBean);
        }
        incomeDetails.get(maxIndex).setHighest(true);

      //初始化recyclerview
        layoutManager = new LinearLayoutManager(this);
       layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
        recyclerView.setLayoutManager(layoutManager);
        barGraphAdapter = new BarGraphAdapter(this,incomeDetails);
        recyclerView.setAdapter(barGraphAdapter);
}

最后一步adapter,只看绑定控件这一部分,

@Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        //如果没有数据会有几条占位图
        if (incomeDetails == null || incomeDetails.size() == 0){
            holder.barGraphItem.setRatio(0);
            holder.tv_time.setText("--");
            holder.tv_amount.setText("--");
            if (View.VISIBLE == holder.iv_highest.getVisibility()) {
                holder.iv_highest.setVisibility(View.INVISIBLE);
                holder.tv_time.setTextSize(12);
                holder.tv_time.setTextColor(Color.parseColor("#999999"));
            }
            holder.tv_amount.setTextColor(Color.LTGRAY);
        }else {
          //绑定数据
            IncomeDetailsBean incomeDetailsBean = incomeDetails.get(position);
            holder.barGraphItem.setRatio(incomeDetailsBean.getRatio());
            holder.tv_time.setText(incomeDetailsBean.getDate());
            holder.tv_amount.setText(incomeDetailsBean.getIncome());
            holder.tv_amount.setTextColor(incomeDetailsBean.getRatio() == 0 ? Color.LTGRAY : context.getResources().getColor(R.color.colorAccent));
            holder.iv_highest.setVisibility(incomeDetailsBean.isHighest() ? View.VISIBLE : View.INVISIBLE);
            holder.tv_time.setTextSize(incomeDetailsBean.isHighest() ? 16 : 12);
            holder.tv_time.setTextColor(incomeDetailsBean.isHighest() ? context.getResources().getColor(R.color.colorAccent) : Color.parseColor("#999999"));
        }
        //缩放动画(x轴方向不变,y轴由0到1增长效果)
        Animation animation = AnimationUtils.loadAnimation(context, R.anim.scale_item);
        holder.barGraphItem.startAnimation(animation);
    }

纵向缩放动画

<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="700"
    android:fromXScale="1.0"
    android:fromYScale="0.0"
    android:pivotY="100%"
    android:toXScale="1.0"
    android:toYScale="1.0">
</scale>

最后是图中用到的配色资源

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#3F51B5</color>
    <color name="colorPrimaryDark">#303F9F</color>
    <color name="colorAccent">@android:color/holo_green_dark</color>
    <color name="colorLightBlue">#3399cc00</color>
    <color name="colorBarColor">#ff669900</color>
    <color name="colorBarBack">#5099cc00</color>
    <color name="colorGradientWhite">#0599cc00</color>
    <color name="colorGradientGreen">#40669900</color>
</resources>

整个过程没有很生僻的点,基本是对Android Drawable、动画及简单自定义控件这些基本技能的综合运用。
希望对你有所帮助,喜欢记得点赞喔。

作者简介:现就职于甜橙金融信息技术部,负责安卓客户端开发工作。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,858评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,372评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,282评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,842评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,857评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,679评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,406评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,311评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,767评论 1 315
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,945评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,090评论 1 350
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,785评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,420评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,988评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,101评论 1 271
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,298评论 3 372
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,033评论 2 355