侧边栏

侧边栏

WaveSideBarView

package sidebar;

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;

import com.self.zsp.hbcs.R;

import java.util.Arrays;

/**
 * @decs: 侧边波浪导航栏
 * @date: 2018/6/23 12:47
 * @version: v 1.0
 */
public class WaveSideBarView extends View {
    /**
     * sp
     */
    private final static int DEFAULT_TEXT_SIZE = 10;
    /**
     * dp
     */
    private final static int DEFAULT_MAX_OFFSET = 80;
    private final static String[] DEFAULT_INDEX_ITEMS = {"#", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L",
            "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"};
    private String[] mIndexItems;
    /**
     * the index in {@link #mIndexItems} of the current selected index item, it's reset to -1 when the finger up
     */
    private int mCurrentIndex = -1;
    /**
     * Y coordinate of the point where finger is touching, the baseline is top of {@link #mStartTouchingArea}
     * it's reset to -1 when the finger up
     */
    private float mCurrentY = -1;
    private Paint mPaint;
    private int mTextColor;
    private float mTextSize;
    /**
     * the height of each index item
     */
    private float mIndexItemHeight;
    /**
     * offset of the current selected index item
     */
    private float mMaxOffset;
    /**
     * {@link #mStartTouching} will be set to true when {@link MotionEvent#ACTION_DOWN} happens in this area, and the side bar should start working.
     */
    private RectF mStartTouchingArea = new RectF();
    /**
     * height and width of {@link #mStartTouchingArea}
     */
    private float mBarHeight;
    private float mBarWidth;
    /**
     * Flag that the finger is starting touching.
     * If true, it means the {@link MotionEvent#ACTION_DOWN} happened but {@link MotionEvent#ACTION_UP} not yet.
     */
    private boolean mStartTouching = false;
    /**
     * If true, the {@link OnSelectIndexItemListener#onSelectIndexItem(String)} will not be called until the finger up.
     * If false, it will be called when the finger down, up and move.
     */
    private boolean mLazyRespond = false;
    /**
     * The position of the side bar, default is {@link #POSITION_RIGHT}.
     * You can set it to {@link #POSITION_LEFT} for people who use phone with left hand.
     */
    private int mSideBarPosition;
    public static final int POSITION_RIGHT = 0;
    public static final int POSITION_LEFT = 1;
    /**
     * The alignment of items, default is {@link #TEXT_ALIGN_CENTER}.
     */
    private int mTextAlignment;
    public static final int TEXT_ALIGN_CENTER = 0;
    public static final int TEXT_ALIGN_LEFT = 1;
    public static final int TEXT_ALIGN_RIGHT = 2;
    /**
     * observe the current selected index item
     */
    private OnSelectIndexItemListener onSelectIndexItemListener;
    /**
     * the baseline of the first index item text to draw
     */
    private float mFirstItemBaseLineY;
    /**
     * for {@link #dp2px(int)} and {@link #sp2px(int)}
     */
    private DisplayMetrics mDisplayMetrics;

    public WaveSideBarView(Context context) {
        this(context, null);
    }

    public WaveSideBarView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public WaveSideBarView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mDisplayMetrics = context.getResources().getDisplayMetrics();
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.WaveSideBarView);
        mLazyRespond = typedArray.getBoolean(R.styleable.WaveSideBarView_sidebar_lazy_respond, false);
        mTextColor = typedArray.getColor(R.styleable.WaveSideBarView_sidebar_text_color, Color.GRAY);
        mMaxOffset = typedArray.getDimension(R.styleable.WaveSideBarView_sidebar_max_offset, dp2px(DEFAULT_MAX_OFFSET));
        mSideBarPosition = typedArray.getInt(R.styleable.WaveSideBarView_sidebar_position, POSITION_RIGHT);
        mTextAlignment = typedArray.getInt(R.styleable.WaveSideBarView_sidebar_text_alignment, TEXT_ALIGN_CENTER);
        typedArray.recycle();
        mTextSize = sp2px(DEFAULT_TEXT_SIZE);
        mIndexItems = DEFAULT_INDEX_ITEMS;
        initPaint();
    }

    private void initPaint() {
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setColor(mTextColor);
        mPaint.setTextSize(mTextSize);
        switch (mTextAlignment) {
            case TEXT_ALIGN_CENTER:
                mPaint.setTextAlign(Paint.Align.CENTER);
                break;
            case TEXT_ALIGN_LEFT:
                mPaint.setTextAlign(Paint.Align.LEFT);
                break;
            case TEXT_ALIGN_RIGHT:
                mPaint.setTextAlign(Paint.Align.RIGHT);
                break;
            default:
                break;
        }
    }

    @Override
    protected 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;
    }

    @Override
    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;
            if (mSideBarPosition == POSITION_LEFT) {
                switch (mTextAlignment) {
                    case TEXT_ALIGN_CENTER:
                        baseLineX = getPaddingLeft() + mBarWidth / 2 + mMaxOffset * scale;
                        break;
                    case TEXT_ALIGN_LEFT:
                        baseLineX = getPaddingLeft() + mMaxOffset * scale;
                        break;
                    case TEXT_ALIGN_RIGHT:
                        baseLineX = getPaddingLeft() + mBarWidth + mMaxOffset * scale;
                        break;
                    default:
                        break;
                }
            } else {
                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;
                    default:
                        break;
                }
            }
            // draw
            canvas.drawText(
                    // item text to draw
                    mIndexItems[i],
                    // baseLine X
                    baseLineX,
                    // baseLine Y
                    baseLineY,
                    mPaint);
        }
        // reset paint
        mPaint.setAlpha(255);
        mPaint.setTextSize(mTextSize);
    }

    /**
     * Calculating the scale factor of the item to draw.
     *
     * @param index the index of the item in array {@link #mIndexItems}
     * @return the scale factor of the item to draw
     */
    private float getItemScale(int index) {
        float scale = 0;
        if (mCurrentIndex != -1) {
            float distance = Math.abs(mCurrentY - (mIndexItemHeight * index + mIndexItemHeight / 2)) / mIndexItemHeight;
            scale = 1 - distance * distance / 16;
            scale = Math.max(scale, 0);
        }
        return scale;
    }

    @SuppressLint("ClickableViewAccessibility")
    @Override
    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;
            default:
                break;
        }
        return super.onTouchEvent(event);
    }

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

    private float dp2px(int dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, this.mDisplayMetrics);
    }

    private float sp2px(int sp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, this.mDisplayMetrics);
    }

    public void setIndexItems(String... indexItems) {
        mIndexItems = Arrays.copyOf(indexItems, indexItems.length);
        requestLayout();
    }

    public void setTextColor(int color) {
        mTextColor = color;
        mPaint.setColor(color);
        invalidate();
    }

    public void setPosition(int position) {
        if (position != POSITION_RIGHT && position != POSITION_LEFT) {
            throw new IllegalArgumentException("the position must be POSITION_RIGHT or POSITION_LEFT");
        }
        mSideBarPosition = position;
        requestLayout();
    }

    public void setMaxOffset(int offset) {
        mMaxOffset = offset;
        invalidate();
    }

    public void setLazyRespond(boolean lazyRespond) {
        mLazyRespond = lazyRespond;
    }

    public void setTextAlign(int align) {
        if (mTextAlignment == align) {
            return;
        }
        switch (align) {
            case TEXT_ALIGN_CENTER:
                mPaint.setTextAlign(Paint.Align.CENTER);
                break;
            case TEXT_ALIGN_LEFT:
                mPaint.setTextAlign(Paint.Align.LEFT);
                break;
            case TEXT_ALIGN_RIGHT:
                mPaint.setTextAlign(Paint.Align.RIGHT);
                break;
            default:
                throw new IllegalArgumentException("the alignment must be TEXT_ALIGN_CENTER,TEXT_ALIGN_LEFT or TEXT_ALIGN_RIGHT");
        }
        mTextAlignment = align;
        invalidate();
    }

    public void setOnSelectIndexItemListener(OnSelectIndexItemListener onSelectIndexItemListener) {
        this.onSelectIndexItemListener = onSelectIndexItemListener;
    }

    public interface OnSelectIndexItemListener {
        /**
         * 侧选
         *
         * @param letter 字母
         */
        void onSelectIndexItem(String letter);
    }
}

主布局

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_below="@+id/rlBook"
    android:background="@color/cutOffRule">

    <widget.EmptyRecyclerView
        android:id="@+id/ervBook"
         ndroid:layout_width="match_parent"
        android:layout_height="match_parent"
         ndroid:overScrollMode="never"
        android:scrollbars="none" />

    <sidebar.WaveSideBarView
        android:id="@+id/wsvBook"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="@dimen/d12"
        app:sidebar_lazy_respond="false"
        app:sidebar_text_color="@color/colorPrimary" />
</RelativeLayout>
app:sidebar_lazy_respond="false" 列表随右侧导航栏滑变
app:sidebar_lazy_respond="true" 列表不随右侧导航栏滑变,随右侧导航栏点变

Item布局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginTop="@dimen/d0.5"
    android:background="@drawable/white_gray_button_en_r_selector">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tvBookStickyDecoration"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@color/cutOffRule"
            android:paddingBottom="@dimen/d6"
            android:paddingLeft="@dimen/d12"
            android:paddingTop="@dimen/d6"
            android:textColor="@color/fontHint"
            android:textSize="@dimen/s14"
            android:visibility="gone"
            tools:text="A" />

        <TextView
            android:id="@+id/tvBookName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingBottom="@dimen/d12"
            android:paddingLeft="@dimen/d12"
            android:paddingTop="@dimen/d12"
            android:textColor="@color/fontInput"
            android:textSize="@dimen/s13"
            tools:text="张三" />

        <TextView
            android:id="@+id/tvBookDescribe"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:paddingBottom="@dimen/d12"
            android:paddingLeft="@dimen/d12"
            android:textColor="@color/fontInput"
            android:textSize="@dimen/s12"
            tools:text="上门照护" />
    </LinearLayout>   
</RelativeLayout>

适配器

@Override
public void onBindViewHolder(@NonNull final ViewHolder holder, int position) {
    final Book.DataBean dataBean = dataBeanList.get(position);
    // 数据存itemView之Tag以便点获
    holder.itemView.setTag(position);
    holder.tvBookDescribe.setText(dataBean.getJabRemark());
    if (position == 0 || !dataBeanList.get(position - 1).getIndex().equals(dataBean.getIndex())) {
        holder.tvBookStickyDecoration.setVisibility(View.VISIBLE);
        holder.tvBookStickyDecoration.setText(dataBean.getIndex());
    } else {
        // 布局该控件GONE,避复用致错乱故需恢复GONE
        holder.tvBookStickyDecoration.setVisibility(View.GONE);
    }
}

主代码

private WaveSideBarView wsvBook;
wsvBook = view.findViewById(R.id.wsvBook);

wsvBook.setOnSelectIndexItemListener(new WaveSideBarView.OnSelectIndexItemListener() {
    @Override
    public void onSelectIndexItem(String letter) {
        for (int i = 0; i < sortList.size(); i++) {
            if (sortList.get(i).getIndex().equals(letter)) {
                ((LinearLayoutManager) ervBook.getLayoutManager()).scrollToPositionWithOffset(i, 0);
                return;
            }
        }
    }
});

侧边栏+列表+粘性装饰

WaveSideBar

package sidebar;

import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.os.Build;
import android.support.v4.content.ContextCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

import com.self.zsp.hbcs.R;

import java.util.Arrays;
import java.util.List;

import value.Magic;

/**
 * @decs: 侧边波浪导航栏
 * @author: 郑少鹏
 * @date: 2018/6/23 15:04
 * @version: v 1.0
 */
public class WaveSideBar extends View {
    /**
     * 贝塞尔曲线角弧长
     */
    private static final double ANGLE = Math.PI * 45 / 180;
    private static final double ANGLE_R = Math.PI * 90 / 180;
    private OnTouchLetterChangeListener mListener;
    /**
     * 渲染字母表
     */
    private List<String> mLetters;
    /**
     * 当前选位
     */
    private int mChoosePosition = -1;
    private int mOldPosition;
    private int mNewPosition;
    /**
     * 字母列表画笔
     */
    private Paint mLettersPaint = new Paint();
    /**
     * 提示字母画笔
     */
    private Paint mTextPaint = new Paint();
    /**
     * 波浪画笔
     */
    private Paint mWavePaint = new Paint();
    private int mTextSize;
    private int mTextColor;
    private int mTextColorChoose;
    private int mWidth;
    private int mHeight;
    private int mItemHeight;
    private int mPadding;
    /**
     * 波浪路径
     */
    private Path mWavePath = new Path();
    /**
     * 圆形路径
     */
    private Path mCirclePath = new Path();
    /**
     * 手指滑Y点作中心点
     */
    private int mCenterY;
    /**
     * 贝塞尔曲线分布半径
     */
    private int mRadius;
    /**
     * 圆形半径
     */
    private int mCircleRadius;
    /**
     * 过渡效果计算
     */
    private ValueAnimator mRatioAnimator;
    /**
     * 贝塞尔曲线比率
     */
    private float mRatio;
    /**
     * 选中字坐标
     */
    private float mPointX, mPointY;
    /**
     * 圆形中心点X
     */
    private float mCircleCenterX;

    public WaveSideBar(Context context) {
        this(context, null);
    }

    public WaveSideBar(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public WaveSideBar(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        mLetters = Arrays.asList(context.getResources().getStringArray(R.array.WaveSideBarLetters));
        mTextColor = Color.parseColor("#969696");
        int mWaveColor = Color.parseColor("#bef9b81b");
        mTextColorChoose = ContextCompat.getColor(context, android.R.color.white);
        mTextSize = context.getResources().getDimensionPixelSize(R.dimen.s10);
        int mHintTextSize = context.getResources().getDimensionPixelSize(R.dimen.s32);
        mPadding = context.getResources().getDimensionPixelSize(R.dimen.d20);
        if (attrs != null) {
            @SuppressLint("CustomViewStyleable") TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.WaveSideBar);
            mTextColor = a.getColor(R.styleable.WaveSideBar_textColor, mTextColor);
            mTextColorChoose = a.getColor(R.styleable.WaveSideBar_chooseTextColor, mTextColorChoose);
            mTextSize = a.getDimensionPixelSize(R.styleable.WaveSideBar_textSize, mTextSize);
            mHintTextSize = a.getDimensionPixelSize(R.styleable.WaveSideBar_hintTextSize, mHintTextSize);
            mWaveColor = a.getColor(R.styleable.WaveSideBar_backgroundColor, mWaveColor);
            mRadius = a.getDimensionPixelSize(R.styleable.WaveSideBar_radius, context.getResources().getDimensionPixelSize(R.dimen.d20));
            mCircleRadius = a.getDimensionPixelSize(R.styleable.WaveSideBar_circleRadius, context.getResources().getDimensionPixelSize(R.dimen.d24));
            a.recycle();
        }
        mWavePaint = new Paint();
        mWavePaint.setAntiAlias(true);
        mWavePaint.setStyle(Paint.Style.FILL);
        mWavePaint.setColor(mWaveColor);
        mTextPaint.setAntiAlias(true);
        mTextPaint.setColor(mTextColorChoose);
        mTextPaint.setStyle(Paint.Style.FILL);
        mTextPaint.setTextSize(mHintTextSize);
        mTextPaint.setTextAlign(Paint.Align.CENTER);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        final float y = event.getY();
        final float x = event.getX();
        mOldPosition = mChoosePosition;
        mNewPosition = (int) (y / mHeight * mLetters.size());
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                // 限触范围
                if (x < mWidth - Magic.FLOAT_YDW * mRadius) {
                    return false;
                }
                mCenterY = (int) y;
                startAnimator(1.0f);
                break;
            case MotionEvent.ACTION_MOVE:
                mCenterY = (int) y;
                if (mOldPosition != mNewPosition) {
                    if (mNewPosition >= 0 && mNewPosition < mLetters.size()) {
                        mChoosePosition = mNewPosition;
                        if (mListener != null) {
                            mListener.onLetterChange(mLetters.get(mNewPosition));
                        }
                    }
                }
                invalidate();
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                startAnimator(0f);
                mChoosePosition = -1;
                break;
            default:
                break;
        }
        return true;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mHeight = MeasureSpec.getSize(heightMeasureSpec);
        mWidth = getMeasuredWidth();
        mItemHeight = (mHeight - mPadding) / mLetters.size();
        mPointX = mWidth - 1.6f * mTextSize;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        // 绘字母列表
        drawLetters(canvas);
        // 绘波浪
        drawWavePath(canvas);
        // 绘圆
        drawCirclePath(canvas);
        // 绘选中字
        drawChooseText(canvas);
    }

    /**
     * 绘字母列表
     *
     * @param canvas 画布
     */
    private void drawLetters(Canvas canvas) {
        RectF rectF = new RectF();
        rectF.left = mPointX - mTextSize;
        rectF.right = mPointX + mTextSize;
        rectF.top = mTextSize / 2;
        rectF.bottom = mHeight - mTextSize / 2;
        mLettersPaint.reset();
        mLettersPaint.setStyle(Paint.Style.FILL);
        mLettersPaint.setColor(Color.parseColor("#F9F9F9"));
        mLettersPaint.setAntiAlias(true);
        canvas.drawRoundRect(rectF, mTextSize, mTextSize, mLettersPaint);
        mLettersPaint.reset();
        mLettersPaint.setStyle(Paint.Style.STROKE);
        mLettersPaint.setColor(mTextColor);
        mLettersPaint.setAntiAlias(true);
        canvas.drawRoundRect(rectF, mTextSize, mTextSize, mLettersPaint);
        for (int i = 0; i < mLetters.size(); i++) {
            mLettersPaint.reset();
            mLettersPaint.setColor(mTextColor);
            mLettersPaint.setAntiAlias(true);
            mLettersPaint.setTextSize(mTextSize);
            mLettersPaint.setTextAlign(Paint.Align.CENTER);
            Paint.FontMetrics fontMetrics = mLettersPaint.getFontMetrics();
            float baseline = Math.abs(-fontMetrics.bottom - fontMetrics.top);
            float pointY = mItemHeight * i + baseline / 2 + mPadding;
            if (i == mChoosePosition) {
                mPointY = pointY;
            } else {
                canvas.drawText(mLetters.get(i), mPointX, pointY, mLettersPaint);
            }
        }
    }

    /**
     * 绘选中字母
     *
     * @param canvas 画布
     */
    private void drawChooseText(Canvas canvas) {
        if (mChoosePosition != -1) {
            // 绘右选中字符
            mLettersPaint.reset();
            mLettersPaint.setColor(mTextColorChoose);
            mLettersPaint.setTextSize(mTextSize);
            mLettersPaint.setTextAlign(Paint.Align.CENTER);
            canvas.drawText(mLetters.get(mChoosePosition), mPointX, mPointY, mLettersPaint);
            // 绘提示字符
            if (mRatio >= Magic.FLOAT_LDJ) {
                String target = mLetters.get(mChoosePosition);
                Paint.FontMetrics fontMetrics = mTextPaint.getFontMetrics();
                float baseline = Math.abs(-fontMetrics.bottom - fontMetrics.top);
                float x = mCircleCenterX;
                float y = mCenterY + baseline / 2;
                canvas.drawText(target, x, y, mTextPaint);
            }
        }
    }

    /**
     * 绘波浪
     *
     * @param canvas 画布
     */
    private void drawWavePath(Canvas canvas) {
        mWavePath.reset();
        // 移至起始点
        mWavePath.moveTo(mWidth, mCenterY - 3 * mRadius);
        // 上部控制点Y轴位
        int controlTopY = mCenterY - 2 * mRadius;
        // 上部结束点坐标
        int endTopX = (int) (mWidth - mRadius * Math.cos(ANGLE) * mRatio);
        int endTopY = (int) (controlTopY + mRadius * Math.sin(ANGLE));
        mWavePath.quadTo(mWidth, controlTopY, endTopX, endTopY);
        // 中心控制点坐标
        int controlCenterX = (int) (mWidth - 1.8f * mRadius * Math.sin(ANGLE_R) * mRatio);
        int controlCenterY = mCenterY;
        // 下部结束点坐标
        int controlBottomY = mCenterY + 2 * mRadius;
        int endBottomY = (int) (controlBottomY - mRadius * Math.cos(ANGLE));
        mWavePath.quadTo(controlCenterX, controlCenterY, endTopX, endBottomY);
        mWavePath.quadTo(mWidth, controlBottomY, mWidth, controlBottomY + mRadius);
        mWavePath.close();
        canvas.drawPath(mWavePath, mWavePaint);
    }

    /**
     * 绘左提示圆
     *
     * @param canvas 画布
     */
    private void drawCirclePath(Canvas canvas) {
        // X轴移路径
        mCircleCenterX = (mWidth + mCircleRadius) - (2.0f * mRadius + 2.0f * mCircleRadius) * mRatio;
        mCirclePath.reset();
        mCirclePath.addCircle(mCircleCenterX, mCenterY, mCircleRadius, Path.Direction.CW);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            mCirclePath.op(mWavePath, Path.Op.DIFFERENCE);
        }
        mCirclePath.close();
        canvas.drawPath(mCirclePath, mWavePaint);
    }

    private void startAnimator(float value) {
        if (mRatioAnimator == null) {
            mRatioAnimator = new ValueAnimator();
        }
        mRatioAnimator.cancel();
        mRatioAnimator.setFloatValues(value);
        mRatioAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator value) {
                mRatio = (float) value.getAnimatedValue();
                // 球弹到位且点击位变,即点时显当前选位
                if (mRatio == Magic.FLOAT_YDL && mOldPosition != mNewPosition) {
                    if (mNewPosition >= 0 && mNewPosition < mLetters.size()) {
                        mChoosePosition = mNewPosition;
                        if (mListener != null) {
                            mListener.onLetterChange(mLetters.get(mNewPosition));
                        }
                    }
                }
                invalidate();
            }
        });
        mRatioAnimator.start();
    }

    public void setOnTouchLetterChangeListener(OnTouchLetterChangeListener listener) {
        this.mListener = listener;
    }

    public List<String> getLetters() {
        return mLetters;
    }

    public void setLetters(List<String> letters) {
        this.mLetters = letters;
        invalidate();
    }

    public interface OnTouchLetterChangeListener {
        /**
         * 字母变
         *
         * @param letter 字母
         */
        void onLetterChange(String letter);
    }
}

TitleItemDecoration

package sidebar;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.TypedValue;
import android.view.View;

import com.self.zsp.hbcs.R;

import java.util.List;
import java.util.Objects;

import bean.Book;

/**
 * @decs: 粘性装饰
 * An ItemDecoration allows the application to add a special drawing and layout offset to specific item views from the adapter's data set.
 * This can be useful for drawing dividers between items, highlights, visual grouping boundaries and more.
 * <p>
 * ItemDecoration主对RecyclerView进行修饰,对adapter数据集中数据视图增修饰或空位。常被用画分割线、强调效果、可见分组边界等。
 * @date: 2018/6/23 15:56
 * @version: v 1.0
 */
public class StickyItemDecoration extends RecyclerView.ItemDecoration {
    private static int TITLE_BG_COLOR;
    private static int TITLE_TEXT_COLOR;
    private List<Book.DataBean> introduceData;
    private Paint paint;
    private Rect bounds;
    private int titleHeight;

    /**
     * constructor
     *
     * @param context 上下文
     * @param data    数据(持引唯一集合变量)
     */
    public StickyItemDecoration(Context context, List<Book.DataBean> data) {
        super();
        introduceData = data;
        paint = new Paint();
        bounds = new Rect();
        titleHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30, context.getResources().getDisplayMetrics());
        TITLE_BG_COLOR = ContextCompat.getColor(context, R.color.grayOther);
        TITLE_TEXT_COLOR = ContextCompat.getColor(context, R.color.warning_color);
        int mTitleTextSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 14, context.getResources().getDisplayMetrics());
        paint.setTextSize(mTitleTextSize);
        paint.setAntiAlias(true);
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth() - parent.getPaddingRight();
        final int childCount = parent.getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            int position = params.getViewLayoutPosition();
            if (position > -1) {
                if (position == 0) {
                    // 等0时绘粘性装饰
                    drawTitle(c, left, right, child, params, position);
                } else {
                    if (null != introduceData.get(position).getIndex() && !introduceData.get(position).getIndex().equals(introduceData.get(position - 1).getIndex())) {
                        // 字母非空且不等前一个绘粘性装饰
                        drawTitle(c, left, right, child, params, position);
                    }
                }
            }
        }
    }

    /**
     * 绘粘性装饰背景和文字
     * 先调绘最下层粘性装饰
     *
     * @param c        画布
     * @param left     left
     * @param right    right
     * @param child    视图
     * @param params   参数
     * @param position 位置
     */
    private void drawTitle(Canvas c, int left, int right, View child, RecyclerView.LayoutParams params, int position) {
        paint.setColor(TITLE_BG_COLOR);
        c.drawRect(left, child.getTop() - params.topMargin - titleHeight, right, child.getTop() - params.topMargin, paint);
        paint.setColor(TITLE_TEXT_COLOR);
        paint.getTextBounds(introduceData.get(position).getIndex(), 0, introduceData.get(position).getIndex().length(), bounds);
        c.drawText(introduceData.get(position).getIndex(), child.getPaddingLeft(), child.getTop() - params.topMargin - (titleHeight / 2 - bounds.height() / 2), paint);
    }

    /**
     * 后调绘最上层粘性装饰
     *
     * @param c      画布
     * @param parent parent
     * @param state  state
     */
    @Override
    public void onDrawOver(Canvas c, final RecyclerView parent, RecyclerView.State state) {
        int position = ((LinearLayoutManager) (parent.getLayoutManager())).findFirstVisibleItemPosition();
        // 搜至无索引时position或等-1,故此需判
        if (position == -1) {
            return;
        }
        String tag = introduceData.get(position).getIndex();
        View child = parent.findViewHolderForLayoutPosition(position).itemView;
        // Canvas位移否
        boolean flag = false;
        if ((position + 1) < introduceData.size()) {
            // 当前第一可见Item字母索引不等其后Item字母索引时切悬浮View
            if (null != tag && !tag.equals(introduceData.get(position + 1).getIndex())) {
                // 当第一可见Item于屏中剩高小于粘性装饰高时开始悬浮粘性装饰动画
                if (child.getHeight() + child.getTop() < titleHeight) {
                    c.save();
                    flag = true;
                    // 下边索引顶上边索引
                    c.translate(0, child.getHeight() + child.getTop() - titleHeight);
                    // 头部折叠(下边索引慢遮上边索引)
                    /*c.clipRect(parent.getPaddingLeft(),
                            parent.getPaddingTop(),
                            parent.getRight() - parent.getPaddingRight(),
                            parent.getPaddingTop() + child.getHeight() + child.getTop());*/
                }
            }
        }
        paint.setColor(TITLE_BG_COLOR);
        c.drawRect(parent.getPaddingLeft(),
                parent.getPaddingTop(),
                parent.getRight() - parent.getPaddingRight(),
                parent.getPaddingTop() + titleHeight, paint);
        paint.setColor(TITLE_TEXT_COLOR);
        paint.getTextBounds(tag, 0, Objects.requireNonNull(tag).length(), bounds);
        c.drawText(tag, child.getPaddingLeft(), parent.getPaddingTop() + titleHeight - (titleHeight / 2 - bounds.height() / 2), paint);
        if (flag) {
            // 恢复画布到前存状
            c.restore();
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        int position = ((RecyclerView.LayoutParams) view.getLayoutParams()).getViewLayoutPosition();
        if (position > -1) {
            switch (position) {
                // 位0绘粘性装饰
                case 0:
                    outRect.set(0, titleHeight, 0, 0);
                    break;
                default:
                    String index = introduceData.get(position).getIndex();
                    String indexFront = introduceData.get(position - 1).getIndex();
                    if (null != index && !index.equals(indexFront)) {
                        // 字母非空且不等前一个绘粘性装饰
                        outRect.set(0, titleHeight, 0, 0);
                    } else {
                        outRect.set(0, 0, 0, 0);
                    }
                    break;
            }
        }
    }
}

布局

<sidebar.WaveSideBar
    android:id="@+id/wsbBook"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:visibility="gone"
    app:backgroundColor="@color/orange"
    app:chooseTextColor="@color/background"
    app:circleRadius="@dimen/d24"
    app:hintTextSize="@dimen/s32"
    app:radius="@dimen/d20"
    app:textColor="@color/fontHint"
    app:textSize="@dimen/s10" />

适配器

/**
 * 据当前位获分类首字母char ASCII值
 */
public int getSectionForPosition(int position) {
    return dataBeanList.get(position).getIndex().charAt(0);
}

/**
 * 据分类首字母char ASCII值获该首字母首现位
 */
public int getPositionForSection(int section) {
    for (int i = 0; i < getItemCount(); i++) {
        String sortStr = dataBeanList.get(i).getIndex();
        char firstChar = sortStr.toUpperCase().charAt(0);
        if (firstChar == section) {
            return i;
        }
    }
    return -1;
}

主代码

private WaveSideBar wsbBook;
wsbBook = view.findViewById(R.id.wsbBook);

wsbBook.setOnTouchLetterChangeListener(new WaveSideBar.OnTouchLetterChangeListener() {
    @Override
    public void onLetterChange(String letter) {
        // 该字母首现位
        int position = bookAdapter.getPositionForSection(letter.charAt(0));
        if (position != -1) {
            ((LinearLayoutManager) ervBook.getLayoutManager()).scrollToPositionWithOffset(position, 0);
        }
    }
});

/*
  false头项/true末项
 */
ervBook.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false));
// 每item内容不改RecyclerView大小,此设提性能
ervBook.setHasFixedSize(true);
// 粘性装饰       
stickyItemDecoration = new StickyItemDecoration(Objects.requireNonNull(getContext()), displayList);
ervBook.addItemDecoration(stickyItemDecoration);       

猜你喜欢

转载自blog.csdn.net/zsp_android_com/article/details/80784057