Android 自定義View 字母索引條

寫在開頭

這是自定義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)的效果就是如圖所示!

index_靜態(tài).png
index.gif

一個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)榛疑?,滑動時選中的字母呈現(xiàn)出選中的狀態(tài),這些都搜easy滴?。‘?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,這樣每個文字水平就是居中顯示的了??!
然后確定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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容