寫在開頭
這是自定義View的第三篇文章白群,第一篇是Android drawPath實(shí)現(xiàn)QQ拖拽泡泡,主要實(shí)現(xiàn)的是題目說的東西娃殖,第二篇是Android 自定義View 跳動的水果和文字,可能看這個題目不知道說的是撒,主要講的是Android drawTextOnPath()
的相關(guān)方法少辣,以及屬性動畫相關(guān)的使用赵哲。當(dāng)然個人覺得動畫效果還是闊以的 嘻嘻总处。。
這篇主要還是說說在onDraw()
中 drawText()
相關(guān)的使用盏缤,實(shí)現(xiàn)的效果就是如圖所示!
一個View從出生到你能看到的話砰蠢,肯定是會經(jīng)歷onMeasure()
、onLayout()
唉铜、onDraw()
,這幾方法的台舱,而自定義View無外乎也要涉及到這幾個相關(guān)的方法,這篇文章沒有那么復(fù)雜,主要涉及的就是onDraw()
方法竞惋!
開門見山-IndexBar
不管是在QQ上柜去,還是在163的郵箱中,或者自己手機(jī)的通訊錄中拆宛,右側(cè)都會躺著一個這個玩意兒嗓奢,我姑且不造官方有沒有相關(guān)的東西,或者大家約定俗成的稱呼這個玩意兒叫什么浑厚,反正我就叫它索引條-IndexBar了吧股耽!
IndexBar從整體樣式上(我觀察的哈),分為兩種钳幅,一種就是不管三七二十一物蝙,26個字母糊糊的貼上去的那種,還有一種就是根據(jù)當(dāng)前的具體內(nèi)容敢艰,只展示相關(guān)的首字母的诬乞!至于touch到IndexBar背景變?yōu)榛疑瑒訒r選中的字母呈現(xiàn)出選中的狀態(tài)钠导,這些都搜easy滴U鸺怠!當(dāng)然你可能要說還有開頭是#號的牡属,或者寫著熱門
等等等的票堵。。
實(shí)現(xiàn)思路
這個問題要一分為二來看湃望,首先是怎么把26個字母畫出來换衬,然后才是怎么去識別觸摸對應(yīng)的是哪個字母!证芭!
畫出對應(yīng)的字母
這個不用多說瞳浦,肯定是要調(diào)用 drawText()
相關(guān)的方法,drawText(@NonNull String text, float x, float y, @NonNull Paint paint)
,這里我們需要注意的就是這里的x和y是撒意思了废士!
它就是控制這個文字開始的左下定位的坐標(biāo)叫潦。文字就是從這個點(diǎn)的開始向右上繪制出來的!Demo中onDraw方法有對應(yīng)的注釋了的方法官硝,打開可以直接看相關(guān)的效果矗蕊。
首先確定X軸的距離,就是(總的寬度-文字的寬度)/2,這樣每個文字水平就是居中顯示的了G饧堋傻咖!
然后確定Y軸的位置,就是(每個文字的總高度+文字的高度)/2岖研,(文字是確定的左下方的坐標(biāo)點(diǎn)卿操,向下應(yīng)該加起來>臁)這樣每個文字在豎直方向單位高度中也是居中顯示的了!害淤!
那么問題來了扇雕,上面說的那些寬度高度等要怎么獲取呢?
獲取屏幕的高度窥摄,平分到26個字母镶奉,有'# '或者 ‘熱門’再把相關(guān)的東西加上!這個就是 每個文字的總高度崭放!接下來就是涉及到這幾個方法:
onDraw()
這個不用說哨苛,你不draw怎么能展示出來呢?
onSizeChanged()
,如果屏幕尺寸發(fā)生了變化币砂,不如說虛擬按鍵隱藏或者展示之后移国,還有就是屏幕旋轉(zhuǎn)相關(guān)的。道伟。
setLetters()
,準(zhǔn)備好了相關(guān)的字母之后,這里就需要去再去計算新的相關(guān)參數(shù)然后通知繪制使碾。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (letters != null) {
for (int i = 0; i < letters.size(); i++) {
String text = letters.get(i);
float textWidth = mPaint.measureText(text);
mPaint.getTextBounds(text, 0, text.length(), mRect);
float textHeight = mRect.height();
float x = mCellWidth * 0.5f - textWidth * 0.5f;
float y = mCellHeight * 0.5f + textHeight * 0.5f + mCellHeight * i + beginY;
mPaint.setColor(mIndex == i ? selecColor : normalColor);
canvas.drawText(text, x, y, mPaint);
}
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mHeight = getMeasuredHeight()-getPaddingTop()-getPaddingBottom();
mCellWidth = getMeasuredWidth();
mCellHeight = mHeight * 1.0f / 26;
if (letters != null) {
beginY = (mHeight - mCellHeight * letters.size()) * 0.5f;
}
}
public void setLetters(@Nullable List<String> letters) {
if (letters == null) {
setVisibility(GONE);
return;
}
this.letters = letters;
mHeight = getMeasuredHeight()-getPaddingTop()-getPaddingBottom();
mCellWidth = getMeasuredWidth();
mCellHeight = mHeight * 1.0f / 26;
beginY = (mHeight - mCellHeight * letters.size()) * 0.5f;
invalidate();
}
setLetters()
和 onSizeChanged()
里面的代碼基本上是重復(fù)的蜜徽,只是在setLetters()
里面調(diào)用了 invalidate()
去通知重新繪制。
觸摸的相關(guān)狀態(tài)添加
首先是觸摸到這個索引條票摇,背景加深拘鞋,這個肯定就是走touch事件了嘛,在ACTION_DOWN
的時候修改相關(guān)狀態(tài)矢门,在ACTION_UP
的時候,再次刷新相關(guān)狀態(tài)咯。
這里要使用refreshDrawableState()
和onCreateDrawableState()
這兩個方法殴泰,如果你知道了芒涡,就當(dāng)我在這里瞎比比吧!哈哈物延。宣旱。如果不清楚,可以看看我之前寫的一篇自定義狀態(tài)選擇器叛薯。
//定義一個狀態(tài)
private static final int[] STATE_FOCUSED = new int[]{android.R.attr.state_focused};
//DOWN 設(shè)為true UP 設(shè)為false
private void refreshState(boolean state) {
if (pressed != state) {
pressed = state;
refreshDrawableState();
}
}
@Override
public int[] onCreateDrawableState(int extraSpace) {
int[] states = super.onCreateDrawableState(extraSpace + 1);
if (pressed) {
mergeDrawableStates(states, STATE_FOCUSED);
}
return states;
}
背景選擇基本歐克了浑吟!
然后是選中的字母的顏色,這個其實(shí)就是更換畫筆的顏色就好了:牧铩组力!這個就放在下面的一塊內(nèi)容中。
點(diǎn)擊相關(guān)回調(diào)
用戶看到的都是表象抖拴,觸摸到的肯定是某一個坐標(biāo)值燎字,這個坐標(biāo)應(yīng)該對應(yīng)這26個字母中的某一個字母的所在的坐標(biāo)!比如說總高度是2600,然后每個字母Y軸所占的區(qū)域就是100轩触,你觸摸的坐標(biāo)是(x,520),那這個明顯就是第六個字母了嘛寞酿,獲取到了對應(yīng)的position,這個問題就解決完了脱柱!
@Override
public boolean onTouchEvent(MotionEvent event) {
float y;
invalidate();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
Log.i("TAG", "onTouchEvent:Down ");
getParent().requestDisallowInterceptTouchEvent(true);
refreshState(true);
y = event.getY();
checkIndex(y);
break;
case MotionEvent.ACTION_MOVE:
y = event.getY();
checkIndex(y);
break;
case MotionEvent.ACTION_UP:
refreshState(false);
mIndex = -1;
break;
default:
break;
}
return true;
}
private void checkIndex(float y) {
int currentIndex;
if (y < beginY+getPaddingTop()) {
return;
}
currentIndex = (int) ((y - beginY-getPaddingTop()) / mCellHeight);
if (currentIndex != mIndex) {
if (mOnLetterChangeListener != null) {
if (letters != null && currentIndex < letters.size()) {
mIndex = currentIndex;
mOnLetterChangeListener.onLetterChange(letters.get(currentIndex));
// Log.i(TAG, "checkIndex: "+letters.get(currentIndex));
}
}
}
}
然后就是上面遺留的那個問題伐弹,選中字母顏色的更改就是通過這個mIndex來實(shí)現(xiàn)的,在draw方法中的這行代碼:
mPaint.setColor(mIndex == i ? selecColor : normalColor);
canvas.drawText(text, x, y, mPaint);
那到這里可以看到榨为,那就是View的展現(xiàn)和相關(guān)邏輯的確是分開的惨好,你看到的都是一些表面現(xiàn)象。
滾動到指定的位置
這個是最終的要求了随闺,這里要區(qū)分實(shí)現(xiàn)機(jī)制了日川,如果你是使用了ListView
,那么直接調(diào)用setSelection()
就可以滾動到指定的位置了矩乐。
如果你是使用了RecycleView
的話龄句,那么就是使用LayoutManager的manager.scrollToPositionWithOffset(pos,0)
。
我在測驗(yàn)中發(fā)現(xiàn)直接使用manager.scrollToPosition()
的話散罕,的確可以滾動分歇,但是不是出現(xiàn)在頂部位置!
總結(jié)
本次Indexbar
的話欧漱,繪制部分主要涉及到了onDraw()
方法职抡,canvas.drawText()
。
細(xì)節(jié)的話误甚,就是onSizeChanged() 和 setLetters()之后的通知重新繪制缚甩。
還有就是狀態(tài)選擇器,兩個方法refreshDrawableState()
和onCreateDrawableState()
窑邦。
需要注意的是擅威,有兩個偏移量 beginY 和 topPadding,beginY是用居中的一個偏移量冈钦,topPadding就不用多說了裕寨!
回調(diào)部分,就是onTouch
相關(guān)處理派继,根據(jù)getY()
獲取相關(guān)Y軸的值推算出對應(yīng)的position,然后再回調(diào)到對應(yīng)的ListView
或者RecycleView
Gif所示的Demo地址:IndexDemo
自定義View的Demo相關(guān)地址自定義View