美文网首页Android开发
WaveSideBar源码分析

WaveSideBar源码分析

作者: Tyler | 来源:发表于2016-12-12 17:54 被阅读97次

    项目地址: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、总结

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

    相关文章

      网友评论

        本文标题:WaveSideBar源码分析

        本文链接:https://www.haomeiwen.com/subject/kkitrttx.html