侧边栏
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);