1. 程式人生 > >安卓開發 簡單實現自定義橫向滾動選擇View : HorizontalselectedView

安卓開發 簡單實現自定義橫向滾動選擇View : HorizontalselectedView

一、需求:

今日產品經理讓在產品裡面加了個橫向選擇的功能,控制元件樣子大致要求為:

這裡寫圖片描述

網上找了好久沒找到此控制元件,只能自己動手寫了,很適合新手練習自定義View,並貢獻給大家,效果如下:

這裡寫圖片描述

其實很多滾輪控制元件也只是這個簡單控制元件 組合一下就可以了 。

有任何問題可以加QQ群詢問:661614986。

二、實現思路:

這裡我偷懶了,沒有把上、左、右三個箭頭寫到控制元件裡面,寫進去也簡單,不過突然感覺在外面佈局,寫個方法出來也是蠻帥的。所以今天我們的主角就是中間的可以橫向滑動的部分,乍一看就是個recycleview,不過這裡我沒有想過要用recycleview來實現,不是不可以,是用recycleview的話,各種判斷、計算偏移量太多了,而且需求中要求只是文字,無需載入佈局,所以為了節省時間就乾脆自定義一個名為HorizontalselectedView的View,寬高無需自己計算,只需在onDraw()方法裡面把每個String 畫出來, 然後監聽滑動事件或者點選左右箭頭的時候,重走onDraw()方法就可以了

,難點在於onDraw的時候,每個String的座標如何獲得

2.1、控制元件特徵:

1、中間箭頭下面的文字(被選中的文字)顏色和字型和其他的不一樣
2、可以橫向左右滑動,滑動過程當中,被選中的文字在變化
3、點選左右箭頭的時候也可以 實現滾動,從而改變被選中的文字
4、可見區域內,顯示的文字數是可以改變的
5、左右滑動的時候有回彈選擇效果

根據以上特徵,就能得到我們所需要的自定義屬性,如下:

    <declare-styleable name="HorizontalselectedView">
        <!--可見數目-->
<attr name="HorizontalselectedViewSeesize" format="integer"></attr> <!--被選擇文字的大小和顏色--> <attr name="HorizontalselectedViewSelectedTextSize" format="float"></attr> <attr name="HorizontalselectedViewSelectedTextColor" format="color|reference"
>
</attr> <!--未被被選擇文字的大小和顏色--> <attr name="HorizontalselectedViewTextSize" format="float"></attr> <attr name="HorizontalselectedViewTextColor" format="color|reference"></attr> </declare-styleable>

在構造方法裡面初始化畫筆和屬性:

    public HorizontalselectedView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        setWillNotDraw(false);//使可以走到onDraw方法
        initAttrs(attrs);//初始化屬性
        initPaint();//初始化畫筆
    }


    /**
     * 初始化屬性
     * @param attrs
     */
    private void initAttrs(AttributeSet attrs) {
        TintTypedArray tta = TintTypedArray.obtainStyledAttributes(getContext(), attrs,
                R.styleable.HorizontalselectedView);
        //兩種字型顏色和字型大小
        seeSize = tta.getInteger(R.styleable.HorizontalselectedView_HorizontalselectedViewSeesize, 5);
        selectedTextSize = tta.getFloat(R.styleable.HorizontalselectedView_HorizontalselectedViewSelectedTextSize, 50);
        selectedColor = tta.getColor(R.styleable.HorizontalselectedView_HorizontalselectedViewSelectedTextColor, context.getResources().getColor(android.R.color.black));
        textSize = tta.getFloat(R.styleable.HorizontalselectedView_HorizontalselectedViewTextSize, 40);
        textColor = tta.getColor(R.styleable.HorizontalselectedView_HorizontalselectedViewTextColor, context.getResources().getColor(android.R.color.darker_gray));
    }


    /**
     * 初始化畫筆
     */
    private void initPaint() {
        textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);//普通文字畫筆
        textPaint.setTextSize(textSize);
        textPaint.setColor(textColor);
        selectedPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);//被選中文字畫筆
        selectedPaint.setColor(selectedColor);
        selectedPaint.setTextSize(selectedTextSize);
    }

2.2、重寫onDraw()方法:

所有的文字裡面,被選中的文字是特殊的,他的大小和顏色不一樣,所有我們先把他給畫出來,關鍵點在於要測量文字的寬高,程式碼如下:

      selectedPaint.getTextBounds(s, 0, s.length(), rect);
            //從矩形區域中讀出文字內容的寬高
            int centerTextWidth = rect.width();
            int centerTextHeight = rect.height();

            canvas.drawText(strings.get(n), getWidth() / 2 - centerTextWidth / 2 ,                 getHeight() / 2 + centerTextHeight / 2, selectedPaint);//繪製被選中文字,注意點是y座標,是以文字底部為中心

下面要做的是就是要遍歷集合,把其他的文字給畫出來,剛才也提到了,座標是難點,我們看張圖來理一理:

這裡寫圖片描述

其實最巧妙的是得到圖中所標註的一個單元的長度anInt,可見區域的長度除以可見個數就得到了 , 還有就是這個n,給到資料來源的時候,我是讓資料來源集合的長度除以2的,這個可以理解的。然後遍歷集合的時候 就可以根據下面得到x的座標:x=width/2+anInt*(i-n)-textWidth/2,這裡面的textWidth我們預設他一樣長了,但實際情況中,可能會出現三位數,四位數,三個字,四個字,所以在程式碼中,我是獲得被選中文字左右兩面長度的平均值得到的。

    if (n > 0 && n < strings.size() - 1) {//獲得中間被選中文字左右兩邊文字寬的平均值
                    textPaint.getTextBounds(strings.get(n - 1), 0, strings.get(n - 1).length(), rect);
                    int width1 = rect.width();
                    textPaint.getTextBounds(strings.get(n + 1), 0, strings.get(n + 1).length(), rect);
                    int width2 = rect.width();
                    textWidth = (width1 + width2) / 2;
                }

接下來就可以在onDraw()方法裡面遍歷了:

            for (int i = 0; i < strings.size(); i++) {//遍歷strings,把每個地方都繪製出來,
                if (n > 0 && n < strings.size() - 1) {
                    textPaint.getTextBounds(strings.get(n - 1), 0, strings.get(n - 1).length(), rect);
                    int width1 = rect.width();
                    textPaint.getTextBounds(strings.get(n + 1), 0, strings.get(n + 1).length(), rect);
                    int width2 = rect.width();
                    textWidth = (width1 + width2) / 2;
                }

                if (i == 0) {//得到高,高度是一樣的,所以無所謂
                    textPaint.getTextBounds(strings.get(0), 0, strings.get(0).length(), rect);
                    textHeight = rect.height();
                }

                if (i != n)
                    canvas.drawText(strings.get(i), (i - n) * anInt + getWidth() / 2 - textWidth / 2 + anOffset, getHeight() / 2 + textHeight / 2, textPaint);//畫出未被選中的每組文字

            }

這樣所得到的效果是把strings平鋪在了控制元件上,其實已經有點樣子了,下面就是進行滑動監聽了。

迷糊了我們休息一下:

這裡寫圖片描述

2.3、觸屏監聽事件:

觸屏監聽,自然而然就是要複寫onTouchEvent 方法了 , 這裡的觸屏事件還是比較簡單的,先貼上程式碼:

 @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downX = event.getX();//獲得點下去的x座標
                break;
            case MotionEvent.ACTION_MOVE://複雜的是移動時的判斷
                float scrollX = event.getX();

               if (scrollX > downX) {
                    //向右滑動,當滑動距離大於每個單元的長度時,則改變被選中的文字。
                    if (scrollX - downX >= anInt) {
                        if (n > 0) {

                             n = n - 1;
                            downX = scrollX;
                        }
                    }
                } else {

                    //向左滑動,當滑動距離大於每個單元的長度時,則改變被選中的文字。
                    if (downX - scrollX >= anInt) {

                        if (n < strings.size() - 1) {

                            n = n + 1;
                            downX = scrollX;
                        }
                    }
                }

                invalidate();
                break;

            case MotionEvent.ACTION_UP:
                //擡起手指時,重繪

                invalidate();

                break;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }

奇怪的是,在這裡面打log,只有down事件觸發,move和up事件觸發不了,這就要加上一行程式碼:

setClickable(true);//使可點選

講解:
down的時候記錄下downX座標值,然後move時根據scrollX 和downX的大小判斷向左還是向右,

從而當scrollX -downX的絕對值剛好等於anInt的時候,n相應的加1 或者減一 , 再進行重繪,繼而把此時的scrollX 賦值給doownX;

這個時候就實現了 左右滑動的時候,整個文字向右或者向左 一個單元一個單元的移動,這顯然不是我們要的效果 , 我們要的是 , 滑動的過程當中 , 文字就跟著手勢在動,所以這裡面肯定需要個偏移量offSet;

   offset = scrollX - downX;

當移動一個單元長度的時候再把offset歸零就可以了;所以最終每個文字的x座標為:

   canvas.drawText(strings.get(i), (i - n) * anInt + getWidth() / 2 - textWidth / 2 + anOffset, getHeight() / 2 + textHeight / 2, textPaint);

那up的時候我們就要把offSet歸零,然後經行重繪,就產生了回彈效果

這樣就基本實現需求了,需要注意的是有幾個地方需要加上n的大小判斷的,不能讓他小於0了,或者大於集合長度了,防止越界,原始碼已上傳至github,點選可檢視。

2.4、對外提供的一些方法:


 /**
     * 設定個數據源
     * @param strings 資料來源String集合
     */
    public void setData(List<String> strings)




  /**
     * 改變中間可見文字的數目
     * @param seeSizes 可見數
     */
    public void setSeeSize(int seeSizes)




    /**
     * 向左移動一個單元
     */
    public void setAnLeftOffset()




    /**
     * 向右移動一個單元
     */
    public void setAnRightOffset()



    /**
     * 獲得被選中的文字
     *
     * @return 被選中的文字
     */
    public String getSelectedString()

三、經驗總結:

本篇文章為適合初級程式設計師學習的典型自定義View,可能很多人看到效果都覺得一頭霧水,不知用了什麼高大上的東西實現的,其實一步步理下來,就是先展示,再讓他動起來,最多就是難在計算座標而已,木戶的地方我們一定要善於畫圖去總結,發現裡面的規律,最後祝各位工作順利,幸福美滿!

安卓交流群:661614986