一個有特點的正六邊形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庫中包括:
(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%,才能清晰觀看。
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)我們需要算出正六邊形豎向之間的距離,請看下圖:
在這裡先說明兩個常量,mLandscapeInterval 和 mVerticalInterval :
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 語言編寫,所以效率比你用普