WaveSideBar源碼分析

項目地址:WaveSideBar 本文分析版本:6adc355

1.簡介

Screenshot

WaveSideBar是一款快速索引導航欄,實現(xiàn)得比較清晰簡單,下面介紹一下使用方法。

2.使用方法

1、在XML中聲明
<android.support.v7.widget.RecyclerView
    android:id="@+id/rv_contacts"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

<com.gjiazhe.wavesidebar.WaveSideBar
    android:id="@+id/side_bar"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingRight="8dp"
    android:paddingLeft="8dp"
    app:sidebar_text_color="#8D6E63"/>
2、在Java代碼中設置回調(diào)
sideBar = (WaveSideBar) findViewById(R.id.side_bar);
sideBar.setOnSelectIndexItemListener(new WaveSideBar.OnSelectIndexItemListener() {
    @Override
    public void onSelectIndexItem(String index) {
        for (int i=0; i<contacts.size(); i++) {
            if (contacts.get(i).getIndex().equals(index)) {
                ((LinearLayoutManager) rvContacts.getLayoutManager()).scrollToPositionWithOffset(i, 0);
                return;
            }
        }
}});

實現(xiàn)快速索引就是這么簡單!

3.源碼分析

WaveSideBar 項目只有一個類 WaveSideBar.java。實現(xiàn)比較簡單,分析此項目的源碼主要是讓自己形成看源碼的習慣。做到知其然,知其所以然。
首先看一下初始化函數(shù):

public WaveSideBar(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mDisplayMetrics = context.getResources().getDisplayMetrics();
    TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.WaveSideBar);
    mLazyRespond = typedArray.getBoolean(R.styleable.WaveSideBar_sidebar_lazy_respond, false);
    mTextColor = typedArray.getColor(R.styleable.WaveSideBar_sidebar_text_color, Color.GRAY);
    mMaxOffset = typedArray.getDimension(R.styleable.WaveSideBar_sidebar_max_offset, dp2px(DEFAULT_MAX_OFFSET));
    mSideBarPosition = typedArray.getInt(R.styleable.WaveSideBar_sidebar_position, POSITION_RIGHT);
    mTextAlignment = typedArray.getInt(R.styleable.WaveSideBar_sidebar_text_alignment, TEXT_ALIGN_CENTER);
    typedArray.recycle();

    mTextSize = sp2px(DEFAULT_TEXT_SIZE);
    mIndexItems = DEFAULT_INDEX_ITEMS;
    initPaint();
}

這部分主要是做了些自定義屬性的初始化工作。

接下來看一下 onMeasure 做了些什么工作。

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int height = MeasureSpec.getSize(heightMeasureSpec);
    int width = MeasureSpec.getSize(widthMeasureSpec);
    Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
    mIndexItemHeight = fontMetrics.bottom - fontMetrics.top;
    mBarHeight = mIndexItems.length * mIndexItemHeight;

    // calculate the width of the longest text as the width of side bar
    for (String indexItem : mIndexItems) {
        mBarWidth = Math.max(mBarWidth, mPaint.measureText(indexItem));
    }

    float areaLeft = (mSideBarPosition == POSITION_LEFT) ? 0 : (width - mBarWidth - getPaddingRight());
    float areaRight = (mSideBarPosition == POSITION_LEFT) ? (getPaddingLeft() + areaLeft + mBarWidth) : width;
    float areaTop = height / 2 - mBarHeight / 2;    float areaBottom = areaTop + mBarHeight;
    mStartTouchingArea.set(
            areaLeft,
            areaTop,
            areaRight,
            areaBottom);

    // the baseline Y of the first item' text to draw
    mFirstItemBaseLineY = (height / 2 - mIndexItems.length * mIndexItemHeight / 2)
            + (mIndexItemHeight / 2 - (fontMetrics.descent - fontMetrics.ascent) / 2)
            - fontMetrics.ascent;
}

height、width 取到控件的寬高, mIndexItemHeight是字體的高度,mBarHeight 計算出總高度,mBarWidth 字符串數(shù)組中最大的的值,mStartTouchingArea保存字符繪制區(qū)域的矩陣,mFirstItemBaseLineY第一個字符繪制的位置。onMeasure中主要還是做初始化的工作,測量出onDraw需要的一些值。

接下來分析一下核心部分onDraw、onTouchEvent:

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    // draw each item
    for (int i = 0, mIndexItemsLength = mIndexItems.length; i < mIndexItemsLength; i++) {
        float baseLineY = mFirstItemBaseLineY + mIndexItemHeight * i;

        // calculate the scale factor of the item to draw
        float scale = getItemScale(i);
        int alphaScale = (i == mCurrentIndex) ? (255) : (int) (255 * (1 - scale));
        mPaint.setAlpha(alphaScale);
        mPaint.setTextSize(mTextSize + mTextSize * scale);
        float baseLineX = 0f;
        switch (mTextAlignment) {
            case TEXT_ALIGN_CENTER:
                baseLineX = getWidth() - getPaddingRight() - mBarWidth / 2 - mMaxOffset * scale;
                break;
            case TEXT_ALIGN_RIGHT:
                baseLineX = getWidth() - getPaddingRight() - mMaxOffset * scale;
                break;
            case TEXT_ALIGN_LEFT:
                baseLineX = getWidth() - getPaddingRight() - mBarWidth - mMaxOffset * scale;
                break;
        }

        // draw
        canvas.drawText(
                mIndexItems[i], //item text to draw
                baseLineX, //baseLine X
                baseLineY, // baseLine Y 
               mPaint);
    }

    // reset paint
    mPaint.setAlpha(255);
    mPaint.setTextSize(mTextSize);
}

onDraw主要流程都在 for 循環(huán)里,每次循環(huán)繪制一個字符。在drawText之前確定 X 坐標和 Y 坐標,設置畫筆mPaint的透明度和字體大小。第一次繪制透明度為0,字體大小是初始值。這兩項的值主要是在 WaveSideBar 的移動過程中改變。下面看一下onTouchEvent的實現(xiàn):

public boolean onTouchEvent(MotionEvent event) {
    if (mIndexItems.length == 0) {
        return super.onTouchEvent(event);
    }

    float eventY = event.getY();
    float eventX = event.getX();
    mCurrentIndex = getSelectedIndex(eventY);

    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            if (mStartTouchingArea.contains(eventX, eventY)) {
                mStartTouching = true;
                if (!mLazyRespond && onSelectIndexItemListener != null) {
                    onSelectIndexItemListener.onSelectIndexItem(mIndexItems[mCurrentIndex]);
                }
                invalidate();
                return true;
            } else {
                mCurrentIndex = -1;
                return false;
            }

        case MotionEvent.ACTION_MOVE:
            if (mStartTouching && !mLazyRespond && onSelectIndexItemListener != null) {
                onSelectIndexItemListener.onSelectIndexItem(mIndexItems[mCurrentIndex]);
            }
            invalidate();
            return true;

        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
            if (mLazyRespond && onSelectIndexItemListener != null) {
                onSelectIndexItemListener.onSelectIndexItem(mIndexItems[mCurrentIndex]);
            }
            mCurrentIndex = -1;
            mStartTouching = false;
            invalidate();
            return true;
    }
    return super.onTouchEvent(event);
}

如果索引數(shù)組值為0直接返回。手指按下后取出X、Y的坐標,根據(jù)Y的坐標計算出是第幾個字符,getSelectedIndex函數(shù)的實現(xiàn)很簡單:

private int getSelectedIndex(float eventY) {
    mCurrentY = eventY - (getHeight() / 2 - mBarHeight / 2);
    if (mCurrentY <= 0) {
        return 0;
    }

    int index = (int) (mCurrentY / this.mIndexItemHeight);
    if (index >= this.mIndexItems.length) {
        index = this.mIndexItems.length - 1;
    }
    return index;
}

繼續(xù)分析onTouchEvent函數(shù),在ACTION_DOWN事件中判斷點擊的坐標是否在字符的矩陣范圍,如果在就調(diào)用回調(diào)函數(shù)把當前位置的字符傳遞過去,如果不在矩陣范圍返回false,因為在ACTION_DOWN事件時返回了false所以就不會接收到后續(xù)的事件(ACTION_MOVE、ACTION_UP)。每次有ACTION_MOVE事件產(chǎn)生,都會去重繪控件,重繪時根據(jù)當前的位置來計算出周邊字符的透明度和TextSize。最后在ACTION_UP中重置一些變量。整個過程基本上就是這樣。

4、總結(jié)

此項目在快速索引的實現(xiàn)上做到了簡單易懂,代碼規(guī)范,擴展性比較好,可以自定義索引內(nèi)容。

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

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

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