WaveSideBar源码分析

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

1.简介

Screenshot

WaveSideBar是一款快速索引导航栏,实现得比较清晰简单,下面介绍一下使用方法。

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代码中设置回调
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;
            }
        }
}});

实现快速索引就是这么简单!

3.源码分析

WaveSideBar 项目只有一个类 WaveSideBar.java。实现比较简单,分析此项目的源码主要是让自己形成看源码的习惯。做到知其然,知其所以然。
首先看一下初始化函数:

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;
}

heightwidth 取到控件的宽高, mIndexItemHeight是字体的高度,mBarHeight 计算出总高度,mBarWidth 字符串数组中最大的的值,mStartTouchingArea保存字符绘制区域的矩阵,mFirstItemBaseLineY第一个字符绘制的位置。onMeasure中主要还是做初始化的工作,测量出onDraw需要的一些值。

接下来分析一下核心部分onDrawonTouchEvent:

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 循环里,每次循环绘制一个字符。在drawText之前确定 X 坐标和 Y 坐标,设置画笔mPaint的透明度和字体大小。第一次绘制透明度为0,字体大小是初始值。这两项的值主要是在 WaveSideBar 的移动过程中改变。下面看一下onTouchEvent的实现:

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);
}

如果索引数组值为0直接返回。手指按下后取出X、Y的坐标,根据Y的坐标计算出是第几个字符,getSelectedIndex函数的实现很简单:

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;
}

继续分析onTouchEvent函数,在ACTION_DOWN事件中判断点击的坐标是否在字符的矩阵范围,如果在就调用回调函数把当前位置的字符传递过去,如果不在矩阵范围返回false,因为在ACTION_DOWN事件时返回了false所以就不会接收到后续的事件(ACTION_MOVEACTION_UP)。每次有ACTION_MOVE事件产生,都会去重绘控件,重绘时根据当前的位置来计算出周边字符的透明度和TextSize。最后在ACTION_UP中重置一些变量。整个过程基本上就是这样。

4、总结

此项目在快速索引的实现上做到了简单易懂,代码规范,扩展性比较好,可以自定义索引内容。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容