用RecycleView實現一個雙重柱狀圖,擴展實現無限輪播

需求

實習第一個星期獲得了一個需求,其中有個模塊就下圖所示的柱狀圖,一開始我對這一塊的想法是上網找個插件去實現輪播圖這個模塊,當作完其餘模塊後,開始柱狀圖模塊的我犯難了。我找了下Android中的柱狀圖的實現都是以MPAndroidChart等爲首的一些開源庫。幾乎全部的柱狀圖的實現都是普通的柱狀圖,而後就是分組形勢的柱狀圖實現,在介紹視頻中我看到了相似我需求的柱狀圖,但我感受好麻煩。由於感受項目總體的模塊就這一塊使用了一個簡單的柱狀圖,感受使用一些開源庫太大材小用了,若是是數據分析與展現的那種頁面須要用到各類圖表展現數據我可能這種本身實現的想法就會小不少。php

看一下這個需求,上一個需求模塊我用RecycleView的網格佈局實現的,我看到這個模塊的時候我以爲啊,它真的也能用RecycleView去實現,我發現其實就是一個橫板的RecycleView,難點呢就在那個條上,就是上面是圓角的矩形,我就想到了使用自定義View去實現這個條的部分。java

根據需求能夠將該模塊的柱狀圖實現,須要使用如下幾個技術:android

1.RecycleViewc++

2.自定義Viewgit

知道了實現技術,就須要實現過程,觀察需求的圖片能夠發現班組一的總人數是最高的,他的背景(橙色)條也是最高的,佔滿了條形區域的所有位置,就能夠知道咱們若是須要實現該功能須要一個比值(總數:全部班組中最大的總人數)。再看內部的條(藍色)的長度,它的規律很好看出,最後能發現是自身頭上兩個參數的比較(在崗:總數)github

開工

自定義柱子

其實我作完後發現難點就在這個自定義柱子上,作到後面我遇到一個坑,是本身對繪圖這一塊有點欠缺的緣由,等說到那再給你們說坑在哪。面試

自定義一個View其實已是基礎,不少面試都會問到,也有不少博客去詳解了,我就不班門弄虎了。canvas

自定義柱子代碼bash

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

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

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

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

    //設置第一層bar的比例
    public void setRatio(double ratio) {
        this.ratio = ratio;
        invalidate();
    }

    //設置內部bar的比例
    public void setBarRatio(double barRatio) {
        this.barRatio = barRatio;
        invalidate();
    }

    //初始化畫筆與設置頂部圓角
    private void initPaint() {
        paint = new Paint();
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(getResources().getColor(R.color.white));
        //抗鋸齒
        paint.setAntiAlias(true);

        gradientDrawable = new GradientDrawable();

        //設置頂部圓角
        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) {
        //第一層bar
        if (ratio != 0){
            canvas.save();
            myDraw(canvas,ratio,getResources().getColor(R.color.colorBarBack));
        }
        //內部bar
        if (barRatio != 0){
            canvas.restore();
            myDraw(canvas,ratio * barRatio,getResources().getColor(R.color.colorBarColor));
        }
    }

    private void myDraw(Canvas canvas,double myRatio,int color) {
        gradientDrawable.mutate();
        gradientDrawable.setColor(color);
        int drawHeight = (int) (measuredHeight * myRatio + 0.5); //四捨五入了下
        //這裏把畫布移到繪畫地方的左上角
        canvas.translate(0,measuredHeight - drawHeight);
        //設置繪製矩形
        gradientDrawable.setBounds(0, 0, measuredWidth, drawHeight);
        gradientDrawable.draw(canvas);
    }

}
複製代碼

放了代碼了,其實挺簡單的,繪製矩形上面的圓角我使用了GradientDrawable的setCornerRadii方法,雖然GradientDrawable呢主要用來設置背景漸變的,GradientDrawable在Android中是shape標籤的動態實現,我就使用他來方便的去實現圓角了,而後咱們在使用shape去實現圓角的時候會有邊緣鋸齒,在初始化畫筆的時候設置個抗鋸齒就要好不少。剛剛說的都是些小小的繪畫細節。ide

而後就是坑的地方了在onDraw中,由於須要繪製兩層柱子才能達到效果,踩坑前的代碼是這樣的:

if (ratio != 0){
    myDraw(canvas,ratio,getResources().getColor(R.color.colorBarBack));
}
if (barRatio != 0){
    myDraw(canvas,ratio * barRatio,getResources().getColor(R.color.colorBarColor));
}
複製代碼

致使效果以下:

前兩組數據差別不大,看不出錯誤,特別是第一組數據的顯示效果徹底沒錯(若是把在崗人數改成220他的顯示效果也不會錯),第三組的數據爲何會這樣呢?致使這樣的緣由是什麼呢?我捋了一遍思路,思路是沒錯的。根據後面一層柱子的顯示效果發現繪製第一層是徹底正確的,只好打印了下每一層柱子繪製的高度,多是計算第二層繪製高度的時候有誤,結果打印結果是正確的,第三組數據內層柱子的繪製高度確實是外層的一半。那確定錯誤的緣由在繪製上面。

錯誤鎖定在了繪製上面就onDraw()中,跳進myDraw(),計算高度沒有任何問題,那繪製本應該沒有問題啊,那整個方法出錯的地方只能是繪製開始的位置了。(也就是下面這個代碼)

//這裏把畫布移到繪畫地方的左上角
canvas.translate(0,measuredHeight - drawHeight);
複製代碼

爲何有坑?

爲何要將畫布移到繪畫地方的左上角呢?

在Android中,canvas就比如一張桌子,桌子左上角有一個Paint(畫筆),每次繪畫都必須從左上角開始,屏幕或者你的View的就像一張固定位置不動的白紙,當你要開始寫字的時候(咱們寫字打印都習慣從左上角開始從左往右的寫字吧),第一步是計算你想在哪開始寫字;第二步是移動桌子,使桌子的左上角移動到開始寫字的地方;第三步纔是根據規則去寫字。

初始化的時候呢,他們就重合了

當咱們先去畫橙色條紋的時候是應該怎樣的呢?(咱們在代碼中沒有移動X軸,卻畫在了中間,實際上是由於畫布的寬度其實就導航欄那麼寬,高度也是固定的爲最高的橙色導航條的高度,因此在後面的原理圖中可能會有些不嚴謹,忽略就好)

若是咱們不移動畫布的繪製結果就會是:

移動後的原理圖呢就是下面這樣的:

Canvas2.png

效果呢就是咱們要的效果了,在Android中座標是倒的,繪畫的起點是在上方,就好比咱們平時畫一棵樹是從底部開始。

可是在計算機中咱們畫一棵樹倒是倒着的,以下圖所示:

這就是與咱們日常不一樣的思惟方式。

其實Android中真實的座標系是:

座標系

因此咱們要想實現同一塊兒點,一塊兒從下面長出來的效果,須要的是計算距離頂部的距離,畫布移動相應的距離再去繪畫其所對應的高。其實就是終點相同,但起點不一樣。

舉個🌰,就像有些人一出生就在終點線了,咱們話的柱狀圖正好是這個事實的體現,出生在終點線附近的人雖然只用畫一點就能到終點線,但人數卻不多,並且他們是父母在本身生前努力了不少,幫他們把畫板移動到了終點線附近的。

那這個爲何會致使上圖的錯誤呢?

簡單的說就是沒有還原最初的狀態。

用一個高級詞彙來解釋,叫作「參考物」不一樣,高中學過運動是相對的,根據參考物的不一樣,物體相對速度也是不一樣的。用上面的爲何將畫布移到左上角的答案來分析。首先畫布(桌子)在View(白紙)的左上角他們是對其的,咱們畫橙色導航條的時候須要將畫布(桌子)移動到開始畫的地方,而後畫布的位置可能已經移動過了(桌子如今可能已經不在左上角了)。咱們畫第二個藍色導航條的時候移動的距離呢,是根據畫布(桌子)在左上角初始的時候算的,若是再移動的話除非第一次畫布(桌子)沒有動(第一組數據總人數是最多的,因此根據需求其佔的比例是1,就致使measuredHeight等於drawHeight),不然就會致使畫布(桌子)移動過頭。

再舉起個🌰來,就是父母幫他們鋪好了路,但他們不太甘心,仍是以一個共同的起點去奮鬥去幫他們的下一代創造將來,但他們是接力跑的,接着父母的腳步繼續前行,就致使了這樣的現象,只要父母沒有努力過的,他們繪畫出來的效果就是正常的。

解決方案

那知道了爲何解決辦法就會有的。兩種方案:

1.選取第一個爲參考物,保留第一次的移動距離,根據第二次所要移動的距離計算出真實的移動距離,移動真實的移動距離。(在本項目中只用計算y軸的相對距離還好,若是加上x軸的話又大大增長了計算量)

2.選取同樣的參考物,還原最初移動前的狀態,你畫完後還完,下一個來不用關心你作了什麼。

咱們固然選第二種啊,使用canvas.save()與canvas.restore(),這就像c++中的push()(c++11中的emplace())與pop()同樣,其實就當作一個狀態棧,第一個畫以前保存一下狀態,畫完恢復畫前的狀態。(我上面代碼是第二個畫以前恢復狀態,效果同樣的,但可能會有些歧義吧,由於若是是第三我的來畫,咱們直接註釋第二我的,若是第三我的不知道要恢復狀態,那這個錯誤就又回來,你們能夠本身改一下,能夠將canvas.save()與canvas.restore()放到myDraw的開始與結束的地方,這樣就增長了代碼的重用性)

咱們最後想要的那個🌰是你們每次的起點相同,每次都從同樣的起點出發。

代碼

最後呢說了那麼多,其實呢就是我被前面那個小小的錯誤坑了,最後就把完整的代碼給到你們吧!(整體實現挺簡單的,後面還有擴展,由於項目需求是TV板的展現,因此就沒有處理onTouch部分,但最後由於可能班組不少,TV板的該模塊須要實現自動滾動效果,我第一想法是像輪播圖同樣,但輪播圖的實現使用的通常是ViewPage,由於ViewPage使用的也是適配器模式,與RecycleView我感受還挺像的,那就將就使用RecycleView去實現輪播的效果了,思想應該都差很少的)

BarChartItem.java

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

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

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

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

    //設置第一層bar的比例
    public void setRatio(double ratio) {
        this.ratio = ratio;
        invalidate();
    }

    //設置內部bar的比例
    public void setBarRatio(double barRatio) {
        this.barRatio = barRatio;
        invalidate();
    }

    //初始化畫筆與設置頂部圓角
    private void initPaint() {
        paint = new Paint();
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(getResources().getColor(R.color.white));
        //抗鋸齒
        paint.setAntiAlias(true);

        gradientDrawable = new GradientDrawable();

        //設置頂部圓角
        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);
        //第一層bar
        if (ratio != 0){
            myDraw(canvas,ratio,getResources().getColor(R.color.colorBarBack));
        }
        //內部bar
        if (barRatio != 0){
            myDraw(canvas,ratio * barRatio,getResources().getColor(R.color.colorBarColor));
        }
    }

    private void myDraw(Canvas canvas,double myRatio,int color) {
        canvas.save();
        gradientDrawable.mutate();
        gradientDrawable.setColor(color);
        int drawHeight = (int) (measuredHeight * myRatio + 0.5); //四捨五入了下
        //這裏把畫布移到繪畫地方的左上角
        canvas.translate(0,measuredHeight - drawHeight);
        //設置繪製矩形
        gradientDrawable.setBounds(0, 0, measuredWidth, drawHeight);
        gradientDrawable.draw(canvas);
        canvas.restore();
    }

}
複製代碼

佈局代碼 item_bar_chart.xml

<?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:layout_marginLeft="10dp" android:layout_marginRight="14dp" android:padding="6dp" android:layout_width="wrap_content" android:layout_height="match_parent">

    <LinearLayout android:gravity="center|bottom" android:layout_weight="1" android:layout_width="wrap_content" android:layout_height="0dp">

        <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="@color/gray" android:text="@string/on_guard" android:textSize="8sp"/>

        <TextView android:id="@+id/on_guard_num" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="@color/gray" android:text="230" android:textSize="8sp"/>

    </LinearLayout>

    <LinearLayout android:gravity="center|top" android:layout_weight="1" android:layout_marginTop="2dp" android:layout_marginBottom="2dp" android:layout_width="wrap_content" android:layout_height="0dp">

        <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="@color/balck" android:text="@string/guard_sum" android:textSize="8sp"/>

        <TextView android:id="@+id/guard_sum" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="@color/balck" android:text="241" android:textSize="8sp"/>

    </LinearLayout>

    <RelativeLayout android:layout_weight="8" android:id="@+id/rl_bar_chart" android:layout_marginTop="4dp" android:layout_marginBottom="4dp" android:layout_width="wrap_content" android:layout_height="0dp">

        <com.example.user.nettydemo.view.BarChartItem android:layout_centerHorizontal="true" android:id="@+id/sum_bar_chart" android:layout_gravity="center_horizontal" android:layout_width="14dp" android:layout_height="match_parent" />

    </RelativeLayout>

    <TextView android:layout_weight="2" android:id="@+id/bar_chart_class_type" android:textColor="@color/balck" android:gravity="center" android:text="班組1" android:textSize="10sp" android:layout_width="match_parent" android:layout_height="0dp" />

</LinearLayout>
複製代碼

JavaBean對象 ClassInfoBean.java

public class ClassInfoBean {
    public static int MaxGuardSum = 0;
    public static int WorkerSum = 2444;
    private String OnJobClassName;
    private int onGuardNum;
    private int GuardSum;

    public void setOnJobClassName(String onJobClassName) {
        OnJobClassName = onJobClassName;
    }

    public void setOnGuardNum(int onGuardNum) {
        this.onGuardNum = onGuardNum;
    }

    public void setGuardSum(int guardSum) {
        GuardSum = guardSum;
        if(GuardSum > MaxGuardSum){
            MaxGuardSum = GuardSum;
        }
    }

    public String getOnJobClassName() {
        return OnJobClassName;
    }

    public int getOnGuardNum() {
        return onGuardNum;
    }

    public int getGuardSum() {
        return GuardSum;
    }

    public int getSumRatio() {
        return 0 == MaxGuardSum ? 0 : GuardSum * 100 / MaxGuardSum;
    }

    public int getOnGuardNumRatio() {
        return 0 == GuardSum ? 0 : onGuardNum * 100 / GuardSum;
    }
}
複製代碼

適配器代碼 BarChartAdapter.java

public class BarChartAdapter extends RecyclerView.Adapter<BarChartAdapter.ViewHolder> {
    private static final String TAG = "BarChartAdapter";

    private List<ClassInfoBean> mClassInfoList = new ArrayList<ClassInfoBean>();

    public BarChartAdapter(List<ClassInfoBean> ClassInfoList){
        mClassInfoList = ClassInfoList;
    }

    static class ViewHolder extends RecyclerView.ViewHolder{
        TextView OnGuardNumTv;
        TextView GuardSumTv;
        BarChartItem barChartItem;
        TextView ClassTypeNameTv;
        public ViewHolder(@NonNull View itemView) {
            super(itemView);
            OnGuardNumTv = (TextView) itemView.findViewById(R.id.on_guard_num);
            GuardSumTv = (TextView) itemView.findViewById(R.id.guard_sum);
            barChartItem = (BarChartItem) itemView.findViewById(R.id.sum_bar_chart);
            ClassTypeNameTv = (TextView) itemView.findViewById(R.id.bar_chart_class_type);
        }
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_bar_chart,parent,false);
        ViewHolder viewHolder = new ViewHolder(view);
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        if(mClassInfoList == null || 0 == mClassInfoList.size()){
            holder.barChartItem.setRatio(0);
            holder.barChartItem.setBarRatio(0);
        }else{
            ClassInfoBean classInfoBean = mClassInfoList.get(position);
            holder.OnGuardNumTv.setText(""+classInfoBean.getOnGuardNum());
            holder.GuardSumTv.setText(""+classInfoBean.getGuardSum());
            holder.barChartItem.setRatio(classInfoBean.getSumRatio()/100.0);
            holder.barChartItem.setBarRatio(classInfoBean.getOnGuardNumRatio()/100.0);
            holder.ClassTypeNameTv.setText(classInfoBean.getOnJobClassName());
        }
    }

    @Override
    public int getItemCount() {
        return mClassInfoList.size();
    }
}
複製代碼

所用到的顏色代碼 colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    
    <color name="white">#FFFFFF</color>
    <color name="colorBarBack">#FFFFCC00</color>
    <color name="colorBarColor">#FF41C7DB</color>
</resources>
複製代碼

MainActivity的實現部分代碼(有時間我就把實現該部分抽出來,作成個小demo給你們參考)

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    private RecyclerView barChartRcv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // BarChart測試
        barChartRcv = (RecyclerView) findViewById(R.id.bar_chart_recycle_view);
        initBarChartView();
        //初始化recyclerview
        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
        linearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
        barChartRcv.setLayoutManager(linearLayoutManager);
        BarChartAdapter barGraphAdapter = new BarChartAdapter(testClassIndoList);
        barChartRcv.setAdapter(barGraphAdapter);
    }

    List<ClassInfoBean> testClassIndoList = new ArrayList<ClassInfoBean>();
    // 測試BarChart
    private void initBarChartView(){
        ClassInfoBean classInfoBean = new ClassInfoBean();
        classInfoBean.setOnJobClassName("班組1");
        classInfoBean.setOnGuardNum(230);
        classInfoBean.setGuardSum(241);
        testClassIndoList.add(classInfoBean);
        ClassInfoBean classInfoBean2 = new ClassInfoBean();
        classInfoBean2.setOnJobClassName("班組2");
        classInfoBean2.setOnGuardNum(180);
        classInfoBean2.setGuardSum(182);
        testClassIndoList.add(classInfoBean2);
        ClassInfoBean classInfoBean3 = new ClassInfoBean();
        classInfoBean3.setOnJobClassName("班組3");
        classInfoBean3.setOnGuardNum(100);
        classInfoBean3.setGuardSum(200);
        testClassIndoList.add(classInfoBean3);
    }

}
複製代碼

這樣就簡單的實現了一個靜態的雙重柱狀圖。

擴展一

上面實現了靜態的需求界面,如今我尚未真實的數據去綁定,因此上面我說的是靜態柱狀圖的實現。而後個人項目是TV板的一個App,那若是班組有多個,超出了範圍確定須要自動滾動去實現。

而後我就先擴展了這部分的代碼。MainActivity.java

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";

    private RecyclerView barChartRcv;

    private LinearLayoutManager bChartlinearLayoutManager;
    private BarChartAdapter barChartAdapter;

    static Handler barChartHandler = new Handler();

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();
        barChartHandler.post(BarChartLooper);
    }

    Runnable BarChartLooper = new Runnable() {
        @Override
        public void run() {
            int index = bChartlinearLayoutManager.findLastVisibleItemPosition();
            index = (index + 1) % barChartAdapter.getItemCount();
            barChartRcv.smoothScrollToPosition(index);
            barChartHandler.postDelayed(this, 2500);
        }
    };


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // BarChart測試
        barChartRcv = (RecyclerView) findViewById(R.id.bar_chart_recycle_view);
        initBarChartView();
        //初始化recyclerview
        bChartlinearLayoutManager = new LinearLayoutManager(this){
            @Override
            public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
                LinearSmoothScroller smoothScroller =
                        new LinearSmoothScroller(recyclerView.getContext()) {
                            @Override
                            protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
                                return 150f / displayMetrics.densityDpi;
                            }
                        };

                smoothScroller.setTargetPosition(position);
                startSmoothScroll(smoothScroller);
            }
        };
        bChartlinearLayoutManager.setOrientation(LinearLayoutManager.HORIZONTAL);
        barChartRcv.setLayoutManager(bChartlinearLayoutManager);
        barChartAdapter = new BarChartAdapter(testClassIndoList);
        barChartRcv.setAdapter(barChartAdapter);
    }

}
複製代碼

這個電腦上沒有錄製gif的工具,我就描述一下,實現了自動滑動了,但我本身還不滿意,不是banner的那種效果,到了最後一個item就返回第一個item了,並且若是item個數較少的話雖然效果上是不動的,可是它仍是去綁定了滑動的功能,還要繼續修改。

擴展二

發現實習後真的學習、寫博客的時間變得有點緊了,我會盡快更新完這篇的,如今的實現我本身都還不滿意,有須要的能夠本身取一下上面靜態部分的實現。(上面代碼部分BarChartAdapter.java只是簡單讓效果出來了,還有不少不足)

未完待續。。。

相關文章
相關標籤/搜索