1. 程式人生 > >一個有特點的正六邊形RecyclerView---HexagonRecyclerView詳解篇

一個有特點的正六邊形RecyclerView---HexagonRecyclerView詳解篇

背景

1.一直有那麼一個衝動,想寫一個自己的控制元件,然後開源在Github,充滿著莫名的成就感。

2.正好朋友的需求給了我靈感,然後對這個控制元件產生了自定義的衝動。

3.本身最近在學習RecyclerView和自定義View,正好可以鞏固一下知識點,並且還有成就感。

4.這個控制元件,看起來挺小眾的,但是你看圖片會感覺這個控制元件挺有創意的,學會了HexagonRecyclerView,那麼類似的奇怪的需求幾乎都沒有問題了。

5.這也是這個月的最後一篇部落格,這個月給自己的小目標也算完成了。

前言

(1)HexagonRecyclerView介紹篇,它是由什麼組成,然後基本的展示.

(2)自定義LayoutManager的基本思路.

(3)HexagonRecyclerView的自定義LayoutManager的過程,包括正六邊形計算相關的拆分.

(4)正六邊形的自定義View詳解.

(5)HexagonRecyclerView後續的一些擴充套件方向.

HexagonRecyclerView介紹篇

HexagonRecyclerView的基本介紹

HexagonRecyclerView是一個由2列正六邊形組成的RecyclerView,可以做側邊索引,可以作為導航欄來使用,是通過自定義LayoutManager,來實現這樣的效果。

這裡寫圖片描述

具體的介紹這裡可以參考Github的ReadMe或者我的這篇部落格:

HexagonRecyclerView的基本組成

雖然名字中帶有RecyclerView,但是HexagonRecyclerView的庫中卻沒有重新定義RecyclerView,而是重新定義LayoutManager,因為LayoutManager是負責RecyclerView的繪製、佈局、測量的。

HexagonRecyclerView

HexagonRecyclerView庫中包括:

(1)PolygonItemView :為自定義的正六邊形View

(2)PolygonLayoutManager : 為RecyclerView展示需要的LayoutManager

(3)Pool : 自定義的View的儲存池,負責View的複用。

(4)MathUtil : 儲存公式的靜態類,負責三角函式公式的呼叫。

HexagonRecyclerView的簡單使用

Adapter的ItemView

    <com.vander.hexlayout.PolygonItemView
        android:id="@+id/itemview"
android:layout_width="110dp" android:layout_height="110dp" app:innerColor="@android:color/white" app:isFull="true" app:outerColor="#f5c421" app:outerWidth="1dp" app:radius="50dp" />

RecyclerView的LayoutManager

        PolygonLayoutManager manager = new PolygonLayoutManager(true);
        manager.setLandscapeInterval(0);
        mMainRv.setLayoutManager(manager);

自定義LayoutManager的思路

其實,當你瞭解LayoutManager之後,你只需要配合一些數學基礎,就能寫出這樣的控制元件。如果你有心,看了這個庫,你會發現其實沒多少程式碼,當然,這個庫能寫出來最大的核心點就是自定義LayoutManager。

那麼下面就開始介紹下自定義LayoutManager的思路:

(1)先設定RecyclerView的子View的LayoutParams引數。

(2)通過Rect將每個子View的測量位置記錄,然後快取起來。

(3)判斷當前Rect是否在螢幕上,如果存在就對其佈局操作即執行layoutDecorate()方法。

(4)設定RecyclerView的滑動方向,分別通過canScrollHorizontally(),canScrollVertically()設定橫向或者豎向能否滑動。

(5)設定RecyclerView的滑動,先設定其上下邊界,然後賦予偏移量,重新佈局即形成滑動效果。

設定子View的LayoutParams引數

首先,要繼承RecyclerView.LayoutManager就必須實現generateDefaultLayoutParams()這個方法,然後設定其子View的LayoutParmas

    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        //比較預設的設定,可以根據自己的需求來定製。
        return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
    }

複寫onLayoutChildren()方法

然後,我們複寫其onLayoutChildren()方法,因為在自定義View的過程中,測量,佈局,繪製是必不可少的步驟,而在完成基本自定義LayoutManager,我們只需要重點關注其佈局和滑動步驟,而onLayoutChildren()作為LayoutManager的佈局方法,在自定義LayoutManager中,屬於最核心的部分。

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        //準備階段
        initLayout();
        //測算階段
        measureLayout();
        //佈局階段
        fill();
    }

在這裡我們可以,將onLayoutChildren()大致分為三個階段,分別為準備階段、測算階段、佈局階段。

(1)第一階段 - 準備階段

    /**
     * onLayoutChildren 準備階段
     * @param recycler
     * @param state
     */
    private void initLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getItemCount() <= 0 || state.isPreLayout()) {
            return;
        }
        //將所有子View先detach一遍,然後標記“Scrap”快取起來
        detachAndScrapAttachedViews(recycler);
    }

(2)第二階段 - 測算階段

    private void measureLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
        //測量每個子View的基本資訊
        View normalView = recycler.getViewForPosition(0);
        measureChildWithMargins(normalView, 0, 0);
        int itemWidth = getDecoratedMeasuredWidth(normalView);
        int itemHeight = getDecoratedMeasuredHeight(normalView);

        for (int i = 0; i < getItemCount(); i++) {
            //mItemFrames為Rect的池物件,就是Rect的一個容器,索引為i。
            Rect item = mItemFrames.get(i);
            .....
            //設定每個子View的Rect的範圍
            .....
        }
    }

在這裡需要自定義一個Rect的池,來儲存生成Rect的範圍,最後用於判斷該View是否處於螢幕上,負責會被回收掉。

(3)第三階段 - 佈局階段

    /**
     * 佈局階段
     *
     * @param recycler
     * @param state
     */
    private void fill(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getItemCount() <= 0 || state.isPreLayout()) {
            return;
        }
        //考慮到當前RecyclerView會處於滑動的狀態,所以這裡的Rect的作用是展示當前顯示的區域
        //需要考慮到RecyclerView的滑動量
        //mHorizontalOffset 橫向的滑動偏移量
        //mVerticalOffset  縱向的滑動偏移量
        Rect displayRect = new Rect(0, mVerticalOffset,
                getHorizontalSpace(),
                getVerticalSpace() + mVerticalOffset);

        /**
         * 對這些View進行測量和佈局操作
         */
        for (int i = 0; i < getItemCount(); i++) {
            Rect frame = mItemFrames.get(i);
            if (Rect.intersects(displayRect, frame)) {
                View scrap = recycler.getViewForPosition(i);
                addView(scrap);
                //測量子View
                measureChildWithMargins(scrap, 0, 0);
                //佈局方法
                layoutDecorated(scrap, frame.left - mHorizontalOffset, frame.top - mVerticalOffset,
                        frame.right - mHorizontalOffset, frame.bottom - mVerticalOffset);
            }
        }
    }

這裡的displayRect是當前顯示的區域,然後我們通過Rect.intersects()方法,判斷子View是否與displayRect相交,如果相交即子View顯示在RecyclerView的展示區域上,然後會對該子View進行佈局操作。

這裡需要注意的是,在計算每個子View的位置時,需要考慮RecyclerView滑動的偏移量。

處理RecyclerView的滑動

其實,經過上面的兩個步驟,RecyclerView顯示已經沒有什麼大的問題了,如果是自定義LayoutManager不需要考慮滑動,其實這樣已經能看到效果了,那麼接下來我們應該處理RecyclerView的滑動,來使控制元件能夠得到更好的體驗。

處理RecyclerView滑動也分兩個階段:

(1)設定控制元件在橫向、豎向上能否滑動。

(2)記錄各個方向上的偏移量,使RecyclerView的各個子View偏移,然後重新進行佈局操作。

(1)第一階段 - 設定控制元件能否滑動

設定RecyclerView在橫向能否滑動:

    @Override
    public boolean canScrollHorizontally() {
        return true;
    }

設定RecyclerView在豎向能否滑動:

    @Override
    public boolean canScrollVertically() {
        return true;
    }

上述兩個方法,根據返回值來決定當前RecyclerView在各個方向上是否可以滑動。

(2)記錄偏移量,並且設定子View偏移,重新進行佈局操作

設定縱向偏移

   @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        detachAndScrapAttachedViews(recycler);
        //上邊界判斷
        if (mVerticalOffset + dy < 0) {
            dy = -mVerticalOffset;
            //下邊界判斷
        } else if (mVerticalOffset + dy > mTotalHeight - getVerticalSpace()) {
            dy = mTotalHeight - getVerticalSpace() - mVerticalOffset;
        }

        mVerticalOffset += dy;
        //使所有ChildView偏移 如果向上滑動 所有View就向下偏移 反之亦然
        offsetChildrenVertical(-dy);
        //重新佈局
        fill(recycler, state);
        return dy;
    }

設定橫向偏移

public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler, RecyclerView.State state) {
    detachAndScrapAttachedViews(recycler);
    //左邊界判斷
    if (mHorizontalOffset + dx < 0) {
        dx = -mHorizontalOffset;
        //右邊界的判斷
    } else if (mHorizontalOffset + dx > mTotalWidth - getHorizontalSpace()) {
        dx = mTotalWidth - getHorizontalSpace() - mHorizontalOffset;
    }
    mHorizontalOffset += dx;
    //使所有ChildView偏移 如果向左滑動 所有View就向右偏移 反之亦然
    offsetChildrenHorizontal(-dx);
    //重新進行佈局操作
    fill(recycler, state);
    return dx;
}

通過上面兩個方法,可以很容易的發現,其實處理橫向或者縱向滑動很簡單,上述兩個方法,幾乎就是模板方法。

不管是橫向、或者豎向滑動,這兩個方法處理的流程幾乎都是一致的,如下:

(1)首先進行邊界判斷,滑動到邊界,就不能繼續在滑動了。

(2)然後記錄橫向或縱向的偏移量,進行offsetChildrenHorizontal()或者offsetChildrenVertical()操作,使子View進行偏移。

(3)最後呼叫fill(),進行佈局操作,完成RecyclerView的滑動處理。

在這裡getHorizontalSpace()、和getVerticalSpace()都屬於輔助方法 :

//測量RecyclerView的整體橫向距離,注意這段距離不包括padding操作,需要減掉
 private int getHorizontalSpace() {
        return getWidth() - getPaddingLeft() - getPaddingRight();
    }

//測量RecyclerView的整體縱向距離,注意這段距離不包括padding操作,需要減掉
private int getVerticalSpace() {
        return getHeight() - getPaddingTop() - getPaddingBottom();
    }

而mTotalWidth 和mTotalHeight 則是在onLayoutChildren()提前計算好的,分別代表LayoutManager中自定義的內容寬度,和高度。

到這裡,其實自定義LayoutManager的基本流程,已經基本完成。

最後來一張圖,總結一下:

友情提示:由於圖片幅度過大,需要放大到150%,才能清晰觀看。

自定義LayoutManager的流程

HexagonRecyclerView的自定義流程

下面的內容,將會著重介紹HexagonRecyclerView這個元件的自定義流程,如果對此感興趣,不妨來一發Star。

HexagonRecyclerView的自定義流程分為兩個階段:

(1)自定義 LayoutManager,以此來控制正六邊形的顯示。

(2)自定義正六邊形 View,以此來高度定製 RecyclerView 中的展示風格。

PolygonLayoutManager的自定義流程

(1)先設定RecyclerView的子View的LayoutParams引數。

    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        return new RecyclerView.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
    }

在這裡對子 View 的大小沒有特殊要求,所以寬高自適應就可以。

(2)onLayoutChildren()的三個流程的實現

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        //準備階段
        initLayout();
        //測算階段
        measureLayout();
        //佈局階段
        fill();
    }

第一階段 : 準備階段

    /**
     * onLayoutChildren 準備階段
     * @param recycler
     * @param state
     */
    private void initLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getItemCount() <= 0 || state.isPreLayout()) {
            return;
        }
        //將所有子View先detach一遍,然後標記“Scrap”快取起來
        detachAndScrapAttachedViews(recycler);
    }

一般來說,這個階段,所需要做的操作,幾乎都是固定的。所以不需要做太大改動。

第二階段 :測算階段

這一階段比較重要,決定著正六邊形的展示形式,可以調整邊距,以及展示位置。

    /**
     * 測量佈局 - 階段
     *
     * @param recycler
     * @param state
     */
    private void measureLayout(RecyclerView.Recycler recycler, RecyclerView.State state) {
        View normalView = recycler.getViewForPosition(0);
        measureChildWithMargins(normalView, 0, 0);

        int itemWidth = getDecoratedMeasuredWidth(normalView);
        int itemHeight = getDecoratedMeasuredHeight(normalView);

        //正六邊形外圓的半徑
        int radius = itemWidth / 2;

        mVerticalInterval = (mLandscapeInterval / MathUtil.sin(60)) - 2 * (radius - radius * MathUtil.sin(60));

        //每組的最大寬度 第一排的寬度加上第二排的寬度
        //這裡的0.75 * itemWidth 和 (3/2)R表達的意思都是一致的.
        int mGroupWidth = (int) (0.75 * itemWidth + itemWidth - mLandscapeInterval);

        if (isGravityCenter && mGroupWidth < getHorizontalSpace()) {
            mGravityOffset = (getHorizontalSpace() - mGroupWidth) / 2;
        } else {
            mGravityOffset = 0;
        }

        for (int i = 0; i < getItemCount(); i++) {
            Rect item = mItemFrames.get(i);
            int offsetHeight = (int) ((i / GROUP_SIZE) * (itemHeight + mVerticalInterval));
            //每組的第一行
            if (isItemInFirstLine(i)) {
                int left = mGravityOffset;
                int top = offsetHeight;
                int right = mGravityOffset + itemWidth;
                int bottom = itemHeight + offsetHeight;
                item.set(left, top, right, bottom);
//                Log.d("第一段的高度", "left : " + left);
//                Log.d("第一段的高度", "top : " + top);
//                Log.d("第一段的高度", "right : " + right);
//                Log.d("第一段的高度", "bottom : " + bottom);
            } else {
                //X軸的偏移是從 正六邊形的外圓 3/2 R出開始偏移
                int itemOffsetWidth = (int) ((3f / 2f) * radius + mLandscapeInterval);
                //Y軸的第一次偏移是 取 (2個正六邊形的寬度 + 中間間距) 得到當前第二排正六邊形的中點 然後往回減 得到的.
                int itemOffsetHeight = (int) ((int) ((2 * itemWidth + mVerticalInterval) / 2) - 0.5 * itemWidth);
                int left = mGravityOffset + itemOffsetWidth;
                int top = itemOffsetHeight + offsetHeight;
                int right = mGravityOffset + itemOffsetWidth + itemWidth;
                int bottom = offsetHeight + itemOffsetHeight + itemHeight;
                item.set(left, top, right, bottom);
//                Log.d("第二段的高度", "left : " + left);
//                Log.d("第二段的高度", "top : " + top);
//                Log.d("第二段的高度", "right : " + right);
//                Log.d("第二段的高度", "bottom : " + bottom);
            }
        }
        //設定總的寬度
        mTotalWidth = Math.max(mGroupWidth, getHorizontalSpace());
        //設定總的高度
        int totalHeight = (int) (getGroupSize() * itemHeight + (getGroupSize() - 1) * mVerticalInterval);
        //判斷當前最後一組如果不是第一行,那麼高度還得加上第一段偏移量
        //Y軸的第一段偏移量
        int itemOffsetHeight = (int) ((int) ((2 * itemWidth + mVerticalInterval) / 2) - 0.5 * itemWidth);
        if (!isItemInFirstLine(getItemCount() - 1)) {
            totalHeight += itemOffsetHeight;
        }
        //設定總的高度 取當前的內容 和 RecyclerView的高度的最大值
        mTotalHeight = Math.max(totalHeight, getVerticalSpace());
    }

在這一階段,主要做了這幾件事:

(1)測量第一個子 View 的寬高,然後獲得當前正六邊形的外接圓半徑。

正六邊形

其實每個正六邊形,都是這麼畫出來的,先做一個輔助的外接圓,然後尋找每一個相交的點就可以。

這裡我們能夠測量出該控制元件的寬為 AB,高為 AD,這時候外接圓的半徑為 AB的一半。

(2)我們需要算出正六邊形豎向之間的距離,請看下圖:

示意圖

在這裡先說明兩個常量,mLandscapeIntervalmVerticalInterval

mLandscapeInterval:代表的是正六邊形形成的正三角形的中線,如圖就是那條藍色的線。

注意:橫向間距這裡是通過自定義實現的,也就是你自己來設定的。

mVerticalInterval:代表的是豎向間距,即等邊三角形的邊長 減去 2倍的 外接圓與正六邊形的邊界差。

我們看自定義的正六邊形圖,可以發現View的邊界不是在正六邊形上,而是在外接圓上。所以需要將多餘的地方減掉。

        mVerticalInterval = (mLandscapeInterval / MathUtil.sin(60)) - 2 * (radius - radius * MathUtil.sin(60));

對比下這兩幅圖,可以很容易的出這樣的公式:

縱向距離 = AB - 2 * (R - R *sin60°)

(3) 計算每組正六邊形的最大寬度,是否小於RecyclerView的寬度,如果小於,則計算出偏移量,使其居中。

        int mGroupWidth = (int) (0.75 * itemWidth + itemWidth - mLandscapeInterval);

        if (isGravityCenter && mGroupWidth < getHorizontalSpace()) {
            mGravityOffset = (getHorizontalSpace() - mGroupWidth) / 2;
        } else {
            mGravityOffset = 0;
        }

這裡需要說明的是,我們指的一組是兩個正六邊形,上圖中正六邊形A、正六邊形C是一組。

然後通過橫向間距,我們可以很好的得出當前的每組的寬度,然後比較。

(4)計算每個Item所處的Rect,這裡的計算分成兩個部分,一個是第一排的Rect,另一個是第二排的Rect,因為橫向間距不同,這裡做一個分別處理。

isItemInFirstLine是判斷當前Item處於第一排還是第二排

第二排的Rect,這裡有幾個特點:

R:外接圓的半徑

(1)left的引數 為(3/2*R + 橫向間距。

(2)top的引數,就是上圖的藍線(形成三角形的中線)的Y軸位置 減去  R/2

(3)(2 * itemHeight+ mVerticalInterval) / 2) 求的就是藍線的Y軸位置,相當於求一箇中點。

(5)最後需要根據item的個數,計算出總的內容長度和高度為計算滑動邊界做準備

 //設定總的寬度
        mTotalWidth = Math.max(mGroupWidth, getHorizontalSpace());
        //設定總的高度
        int totalHeight = (int) (getGroupSize() * itemHeight + (getGroupSize() - 1) * mVerticalInterval);
        //判斷當前最後一組如果不是第一行,那麼高度還得加上第一段偏移量
        //Y軸的第一段偏移量
        int itemOffsetHeight = (int) ((int) ((2 * itemWidth + mVerticalInterval) / 2) - 0.5 * itemWidth);
        if (!isItemInFirstLine(getItemCount() - 1)) {
            totalHeight += itemOffsetHeight;
        }
        //設定總的高度 取當前的內容 和 RecyclerView的高度的最大值
        mTotalHeight = Math.max(totalHeight, getVerticalSpace());

第三階段 :佈局階段

這一階段,最為核心的方法是layoutDecorated()方法,因為LayoutManager是通過這個方法,來給RecyclerView的子View進行佈局操作的。

   /**
     * 第三階段 - 佈局階段
     *
     * @param recycler
     * @param state
     */
    private void fill(RecyclerView.Recycler recycler, RecyclerView.State state) {
        if (getItemCount() <= 0 || state.isPreLayout()) {
            return;
        }
        //考慮到當前RecyclerView會處於滑動的狀態,所以這裡的Rect的作用是展示當前顯示的區域
        //需要考慮到RecyclerView的滑動量
        //mHorizontalOffset 橫向的滑動偏移量
        //mVerticalOffset  縱向的滑動偏移量
        Rect displayRect = new Rect(0, mVerticalOffset,
                getHorizontalSpace(),
                getVerticalSpace() + mVerticalOffset);

        /**
         * 對這些View進行測量和佈局操作
         */
        for (int i = 0; i < getItemCount(); i++) {
            Rect frame = mItemFrames.get(i);
            if (Rect.intersects(displayRect, frame)) {
                View scrap = recycler.getViewForPosition(i);
                addView(scrap);
                //測量子View
                measureChildWithMargins(scrap, 0, 0);
                //佈局方法
                layoutDecorated(scrap, frame.left - mHorizontalOffset, frame.top - mVerticalOffset,
                        frame.right - mHorizontalOffset, frame.bottom - mVerticalOffset);
            }
        }
    }

這裡displayRect 就是滑動過後顯示的區域Rect,然後我們通過Rect.intersects()方法,判斷當前的Rect,是否與displayRect相交,來判斷當前的Item是否顯示在當前的視窗上。

如果相交,然後測量View,將子View佈局在RecyclerView上。

到這裡,onLayoutChildren()已經重寫完畢,現在可以執行可以檢視,自定義的佈局的狀況,但是無法滑動,因為我們還沒有給LayoutManager設定滑動的效果。

(3)設定RecyclerView的滑動效果

設定滑動效果,就比較固定了,有點類似模板的方法,如下:

    @Override
    public boolean canScrollVertically() {
        return true;
    }

    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        detachAndScrapAttachedViews(recycler);
        //上邊界判斷
        if (mVerticalOffset + dy < 0) {
            dy = -mVerticalOffset;
            //下邊界判斷
        } else if (mVerticalOffset + dy > mTotalHeight - getVerticalSpace()) {
            dy = mTotalHeight - getVerticalSpace() - mVerticalOffset;
        }

        mVerticalOffset += dy;
        //使所有ChildView偏移 如果向上滑動 所有View就向下偏移 反之亦然
        offsetChildrenVertical(-dy);
        //重新佈局
        fill(recycler, state);

        return dy;
    }

這裡只希望RecyclerView豎向滑動,不希望橫向滑動,所以只設置了canScrollVertically()為true。

而scrollVerticallyBy()中無非是三個步驟:

(1) 邊界判斷

(2) 記錄滑動值,使子View偏移

(3) 重新根據滑動後的狀態,進行佈局

到此,LayoutManager已經自定義完畢了,順著這個思路下來,可以發現,其實和第一部分介紹自定義LayoutManager一個模式,都是一些通用的流程,然後加上一些輔助計算的常量,就可以實現需要的效果。

其實自定義LayoutManager並沒有那麼難,瞭解了這些你也能寫出屬於你的LayoutManager。

PolygonItemView的自定義流程

大家讀完上面,其實對自定義LayoutManager有一定印象了,那麼直接給Item設定一張圖片背景正常就可以結束了。這樣只能說too young了。

點選區域重合

點選區域重合

通過上圖大家可以發現,這裡的點選區域發生了重合,這時候就會存在問題。所以這時候才會選擇自定義View。

而且自定義View相比使用圖片背景,會有很多優點,也可以高度定製。

1.可以定製正六邊形的邊框、顏色。
2.可以定製正六邊形的大小。
3.可以定製正六邊形的內的背景顏色。

PolygonItemView的初始化

    private void initData() {
        //初始化外邊框的畫筆
        mOuterPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mOuterPaint.setStyle(Paint.Style.STROKE);
        mOuterPaint.setStrokeWidth(mOuterWidth);
        mOuterPaint.setColor(mOuterColor);

        //初始化內側的畫筆
        mInnerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mInnerPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mInnerPaint.setColor(mInnerColor);

        //判斷點選事件是否在範圍內的region
        mRegion = new Region();
        //繪製正六邊形的path
        mViewPath = new Path();
    }

在初始化的過程中,主要初始化各類的繪製引數,包括畫筆,已經繪製路徑的path,和判斷其是否在點選區域內的Region。

繪製正六邊形

    /**
     * 繪製多邊形
     */
    public void lineMultShape(int count) {
        if (count < POLYGON_COUNT) {
            return;
        }
        mViewPath.reset();
        for (int i = 0; i < count; i++) {
            //當前角度
            int angle = 360 / count * i;
            if (i == 0) {
                mViewPath.moveTo(mCenterX + mRadius * MathUtil.cos(angle), mCenterY + mRadius * MathUtil.sin(angle));
            } else {
                mViewPath.lineTo(mCenterX + mRadius * MathUtil.cos(angle), mCenterY + mRadius * MathUtil.sin(angle));
            }
        }
        mViewPath.close();
    }

正六邊形

看了這樣圖,就可以很好的畫出正六邊形了,將360度分成6分,然後( mCenterX + R*cos α ,mCenterY + R * sinα)就是正六邊形的每個端點的座標,在用Path依次將其連線就可以了。

判斷點選區域

其實自定義View,主要解決的是點選區域重合的問題,這裡應該在dispatchTouchEvent()中攔截掉事件,然後判斷其點選區域是否在正六邊形內,來解決點選重合。

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            if (!isEventInPath(event)) {
                return false;
            }
        }
        return super.dispatchTouchEvent(event);
    }
    /**
     * 判斷是否在多邊形內
     *
     * @param event
     * @return
     */
    private boolean isEventInPath(MotionEvent event) {
        RectF bounds = new RectF();
        //計算Path的邊界
        mViewPath.computeBounds(bounds, true);
        //將邊界賦予Region中
        mRegion.setPath(mViewPath, new Region((int) bounds.left, (int) bounds.top,
                (int) bounds.right, (int) bounds.bottom));
        //判斷 當前的觸控點是否在這個範圍內
        return mRegion.contains((int) event.getX(), (int) event.getY());
    }

到此,自定義View,也介紹完畢了。相比於自定義LayoutManager,自定義View更好理解一點。在這裡如果有想法可以將正六邊形換成其他的圖形,完成屬於你自己的創作。

HexagonRecyclerView後續的一些擴充套件方向

(1)從PolygonLayoutManager上,後續會提供多列的RecyclerView,通過設定引數來控制列數。

(2)在自定義View上,可能會提供設定圖片的功能。

(3)後續可能會提供更多種不同形式的RecyclerView,其實原理都是類似的,更多的是創意。

參考文章

RecyclerView自定義LayoutManager,打造不規則佈局

注意:這篇文章有一個BUG,博主可能沒看回復,大家如果參考下,注意我在樓下的回覆。

http://blog.csdn.net/qibin0506/article/details/52676670#reply

相關推薦

一個特點六邊形RecyclerView---HexagonRecyclerView

背景 1.一直有那麼一個衝動,想寫一個自己的控制元件,然後開源在Github,充滿著莫名的成就感。 2.正好朋友的需求給了我靈感,然後對這個控制元件產生了自定義的衝動。 3.本身最近在學習RecyclerView和自定義View,正好可以鞏固一下知識點

PHP則表達式

-s span 詳解 ont 字符 常用 正則表達式 pla 方括號 一、常用函數: 1、pre_match(參數A,參數B),參數A為正則規則,參數B為被驗證的字符串,符合驗證規則則返回1,否則返回0。 2、preg_replace(參數A,參數B,參數C),參數A為正

Java則表達式+練習

ont 愛愛 方便 綜合 細節 moved move .... 保存 一、導讀   正則表達式,又稱規則表達式。(英文名Regular Expression,所以代碼中常以regex、regexp、RE表示)。正則表達式簡單說就是用於操作文本數據的規則表達式,在Java中我

Python 則表達式與 re 模塊的使用

1.3 個數 介紹 date 點號 name 檢查 模塊 大小寫 強烈推薦正則表達式在線測試網站: https://regex101.com/ 1. 標準庫模塊 re 更多詳情參考官方文檔: https://docs.python.org/3/howto/rege

Python 則表示式模組

由於最近需要使用爬蟲爬取資料進行測試,所以開始了爬蟲的填坑之旅,那麼首先就是先系統的學習下關於正則相關的知識啦。所以將下面正則方面的知識點做了個整理。語言環境為Python。主要講解下Python的Re模組。 下面的語法我就主要列出一部分,剩下的在python官網直接查閱即可:docs.python.org

Python爬蟲學習必備知識點:則表示式模組

一、基礎語法總結 1.1、匹配單個字元 a . d D w W s S [...] [^...] 匹配單個字元(.) 規則:匹配除換行之外的任意字元 In [24]: re.findall("f.o","foo is not fao") Out[24]: ['foo',

jquery中一個點選事件累計觸發問題

最近維護老的公司專案,jquery中事件累計觸發的bug是一個老生長談的問題,因此想要弄清楚就必須先弄清楚addEventListener和onclick系列方法的區別 W3C上原文如下 addEventListener is the way to register

QT---之則表示式QRegExp

引言     正則表示式(regular expression)就是用一個“字串”來描述一個特徵,然後去驗證另一個“字串”是否符合這個特徵。比如 表示式“ab+” 描述的特徵是“一個 'a' 和 任意個 'b' ”,那麼 'ab', 'abb', 'abbbbbbbbb

5-1則基本知識

3.3 正則表示式 本節我們看一下正則表示式的相關用法,正則表示式是處理字串的強大的工具,它有自己特定的語法結構,有了它,實現字串的檢索、替換、匹配驗證都不在話下。 當然對於爬蟲來說,有了它,我們從 HTML 裡面提取我們想要的資訊就非常方便了。 1. 例項引入 說了這麼多,可能我們

Android ,MVP+retrofit +rxjava+glide recyclerview使用 ,條目點選 長按點選,三種管理器 ,分割線

首先是對應的依賴  implementation 'com.android.support:recyclerview-v7:26.1.0' 下面是對應的介面卡 裡面對應的 有點選的註釋 public class HomeAdaper extends RecyclerV

一個很有意思的並查集

並查集是我暑假從高手那裡學到的一招,覺得真是太精妙的設計了。以前我無法解決的一類問題竟然可以用如此簡單高效的方法搞定。不分享出來真是對不起party了。(party:我靠,關我嘛事啊?我跟你很熟麼?) 首先在地圖上給你若干個城鎮,這些城鎮都可以看作點,然後告訴你哪些對城

MySQL之聚合查詢、子查詢、合併查詢、則表示式查詢

一:聚合查詢 1:MySQL之聚合函式 基本表orderitems表結構如下: 2:count()函式 2.1:count()函式用來統計記錄的條數 2.2:與group by 關鍵字一起使用 SQL語句如下: 查詢的結果如下: 3:su

java執行緒池執行返回值執行緒原始碼

java執行緒池提供了幾種執行執行緒的方式,這裡主要討論關於執行有返回值的執行緒的實現原理 方式一: 使用方式: ExecutorService executorService = Executors.newSingleThreadExecutor(); Future fut

什麼是pom.xml?什麼作用?--pom.xml

什麼是POM? POM是專案物件模型(Project Object Model)的簡稱,它是Maven專案中的檔案,使用XML表示,名稱叫做pom.xml。作用類似ant的build.xml檔案,功能更強大。該檔案用於管理:原始碼、配置檔案、開發者的資訊和角色、問題追蹤系統

Python3 如何優雅地使用則表示式(一)

正則表示式介紹 正則表示式(Regular expressions 也稱為 REs,或 regexes 或 regex patterns)本質上是一個微小的且高度專業化的程式語言。它被嵌入到 Python 中,並通過 re 模組提供給程式猿使用。使用正則表示式,你需要指定一些規則來描述那些你

Python3 如何優雅地使用則表示式(六)

上一篇:Python3 如何優雅地使用正則表示式(詳解五)   修改字串 我們已經介紹完如何對字元進行搜尋,接下來我們講講正則表示式如何修改字串。 正則表示式使用以下方法修改字串: 方法 用途 split(

Python3 如何優雅地使用則表示式(五)

上一篇:Python3 如何優雅地使用正則表示式(詳解四)   非捕獲組和命名組 精心設計的正則表示式可能會劃分很多組,這些組不僅可以匹配相關的子串,還能夠對正則表示式本身進行分組和結構化。在複雜的正則表示式中,由於有太多的組,因此通過組的序號來跟蹤和使用會變得困難。有兩個

Python3 如何優雅地使用則表示式(四)

上一篇:Python3 如何優雅地使用正則表示式(詳解三)   更多強大的功能 到目前為止,我們只是介紹了正則表示式的一部分功能。在這一篇中,我們會學習到一些新的元字元,然後再教大家如何使用組來獲得被匹配的部分文字。   更多元字元 還有一些元字元我們

Python3 如何優雅地使用則表示式(三)

上一篇:Python3 如何優雅地使用正則表示式(詳解二) 模組級別的函式 使用正則表示式也並非一定要建立模式物件,然後呼叫它的匹配方法。因為,re 模組同時還提供了一些全域性函式,例如 match(),search(),findall(),sub() 等等。這些函式的第一個引數是正則表

Python3 如何優雅地使用則表示式(二)

上一篇:Python3 如何優雅地使用正則表示式(詳解一) 使用正則表示式 現在我們開始來寫一些簡單的正則表示式吧。Python 通過 re 模組為正則表示式引擎提供一個介面,同時允許你將正則表示式編譯成模式物件,並用它們來進行匹配。解釋:re 模組是使用 C 語言編寫,所以效率比你用普