/**
* 成员信息列表 -右侧的导航条
*/
class EnglishIndexBar @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {
private var mIndex = -1
private var mTextSize: Int = 0
private var mSelectTextColor: Int = 0
private var mSelectTextSize: Int = 0
private var mHintTextColor: Int = 0
private var mHintTextSize: Int = 0
private var mHintCircleRadius: Int = 0
private var mWaveRadius: Int = 0
private var mContentPadding: Int = 0
private var mBarWidth: Int = 0
private var mIndexWord : String? = "A"
private var isUpdateView : Boolean = false
private var mSlideBarRect: RectF= RectF()
private lateinit var mTextPaint: TextPaint
private lateinit var mPaint: Paint
private lateinit var mWavePaint: Paint
private lateinit var mHintPaint: Paint
private var mSelect: Int = 0
private var mPreSelect: Int = 0
private var mNewSelect: Int = 0
private lateinit var mTtextBgPaint: Paint
private var mRatioAnimator: ValueAnimator? = null
private var mAnimationRatio: Float = 0.toFloat()
private var mListener: OnLetterChangeListener? = null
private var mTouchY: Int = 0
private lateinit var mBitmap: Bitmap
private var mIsActionDown: Boolean = false
private var mIsShowWave: Boolean = true
private var mViewPager: NoScrollViewPager? = null
private var mLetters = arrayOf("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") //设置字母索引数据
init {
initAttribute(attrs, defStyleAttr)
initData()
}
private fun initAttribute(attrs: AttributeSet?, defStyleAttr: Int) {
val typeArray = context.obtainStyledAttributes(attrs, R.styleable.EnglishIndexBarTwo, defStyleAttr, 0)
mTextSize = typeArray.getDimensionPixelOffset(R.styleable.EnglishIndexBarTwo_textSize, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 9f,
resources.displayMetrics).toInt())
mSelectTextColor = typeArray.getColor(R.styleable.EnglishIndexBarTwo_selectTextColor, Color.parseColor("#FFFFFF"))
mSelectTextSize = typeArray.getDimensionPixelOffset(R.styleable.EnglishIndexBarTwo_selectTextSize, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 10f,
resources.displayMetrics).toInt())
mHintTextColor = typeArray.getColor(R.styleable.EnglishIndexBarTwo_hintTextColor, Color.parseColor("#FFFFFF"))
mHintTextSize = typeArray.getDimensionPixelOffset(R.styleable.EnglishIndexBarTwo_hintTextSize,
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16f,
resources.displayMetrics).toInt())
mHintCircleRadius = typeArray.getDimensionPixelOffset(R.styleable.EnglishIndexBarTwo_hintCircleRadius,
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 24f,
resources.displayMetrics).toInt())
mWaveRadius = 20
mContentPadding = 2
mBarWidth = typeArray.getDimensionPixelOffset(R.styleable.EnglishIndexBarTwo_barWidth,
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 0f,
resources.displayMetrics).toInt())
if (mBarWidth == 0) {
mBarWidth = 2 * mTextSize
}
typeArray.recycle()
}
@TargetApi(Build.VERSION_CODES.KITKAT)
private fun initData() {
mTextPaint = TextPaint()
mPaint = Paint()
mTextPaint.isAntiAlias = true
mPaint.isAntiAlias = true
mTtextBgPaint = Paint()
mTtextBgPaint.isAntiAlias = true
mWavePaint = Paint()
mWavePaint.isAntiAlias = true
mHintPaint = Paint()
mSelect = -1
mBitmap = BitmapFactory.decodeResource(resources, R.drawable.bg_index_water)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val contentLeft = (measuredWidth - mBarWidth ).toFloat()
val contentRight = (measuredWidth ).toFloat()
//val contentTop = mBarPadding.toFloat()
val contentBottom = (measuredHeight).toFloat()
mSlideBarRect.set(contentLeft, 0f, contentRight, contentBottom)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
//绘制slide bar 上字母列表
drawLetters(canvas)
}
/**
* 绘制slide bar 上字母列表
*/
private fun drawLetters(canvas: Canvas) {
//绘制圆角矩形
mPaint.style = Paint.Style.FILL
mPaint.color = Color.parseColor("#00000000")
canvas.drawRoundRect(mSlideBarRect, mBarWidth / 2.0f, mBarWidth / 2.0f, mPaint)
//绘制描边
canvas.drawRoundRect(mSlideBarRect, mBarWidth / 2.0f, mBarWidth / 2.0f, mPaint)
//顺序绘制文字
val itemHeight = (mSlideBarRect.bottom - mSlideBarRect?.top - (mContentPadding * 2).toFloat()) / mLetters.size
for (index in mLetters.indices) {
val baseLine = getTextBaseLineByCenter(
mSlideBarRect.top + mContentPadding.toFloat() + itemHeight * index + itemHeight / 2, mTextPaint, mTextSize)
mTextPaint.textSize = mTextSize.toFloat()
mTextPaint.textAlign = Paint.Align.CENTER
val pointX = mSlideBarRect.left + (mSlideBarRect.right - mSlideBarRect.left) / 2.0f
if(mLetters.contains(mIndexWord) && isUpdateView && mIndex == index){ //标识列表已经匹配的字母
isUpdateView = false
mTtextBgPaint.color = Color.parseColor("#61BE82")
mTextPaint.color = Color.parseColor("#FFFFFF")
if(mSelect != -1 && mSelect < mLetters.size){ // 绘制提示字符
if(mIsShowWave) {
canvas.drawBitmap(mBitmap, pointX - SystemUtil.sp2px(context, 48),
baseLine - SystemUtil.sp2px(context, 15), mWavePaint)
}
if (mSelect != -1) {
mHintPaint.color = mHintTextColor
mHintPaint.textSize = SystemUtil.sp2px(context,15).toFloat()
mHintPaint.textAlign = Paint.Align.CENTER
canvas.drawText(mLetters[index], pointX-SystemUtil.sp2px(context,35), baseLine+SystemUtil.sp2px(context,3), mHintPaint)
}
}
}else{
mTtextBgPaint.color = Color.parseColor("#00000000")
mTextPaint.color = Color.parseColor("#555555")
}
canvas.drawCircle( pointX, baseLine-dp2px(3),SystemUtil.sp2px(context,7).toFloat(),mTtextBgPaint)
canvas.drawText(mLetters[index], pointX, baseLine, mTextPaint)
}
}
/**
* dp转px
* @param dpValue
* @return
*/
fun dp2px(dpValue: Int): Int {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue.toFloat(), resources.displayMetrics).toInt()
}
/**
* 通过获取到的字母来匹配字母检索列表的字母背景
* @parem Words
*/
fun notifiChangeIndexWordBg(word : String?){
if(mLetters.contains(word)){
this.mIndexWord = word
this.mIndex = mLetters.indexOf(word)
invalidate() //刷新界面,只会调用onDraw()方法
isUpdateView = true
}
}
/**
* 给定文字的center获取文字的base line
*/
private fun getTextBaseLineByCenter(center: Float, paint: TextPaint, size: Int): Float {
paint.textSize = size.toFloat()
val fontMetrics = paint.fontMetrics
val height = fontMetrics.bottom - fontMetrics.top
return center + height / 2 - fontMetrics.bottom
}
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
val y = event.y
val x = event.x
mPreSelect = mSelect
mNewSelect = (y / (mSlideBarRect.bottom - mSlideBarRect.top) * mLetters.size).toInt()
when (event.action) {
MotionEvent.ACTION_DOWN -> {
//保证down的时候在bar区域才相应事件
mViewPager?.setNoScroll(true)
if (x < mSlideBarRect.left || y < mSlideBarRect.top || y > mSlideBarRect.bottom) {
mIsActionDown = true
mIsShowWave = true
if(mNewSelect != -1 && mNewSelect < mLetters.size) {
mSelect = mNewSelect
if (mListener != null) {
mListener?.onLetterChange(mLetters[mNewSelect])
}
notifiChangeIndexWordBg(mLetters[mNewSelect])
}
val circleCenterX = measuredWidth + mHintCircleRadius - (2.0f * mWaveRadius
+ 2.0f * mHintCircleRadius) * mAnimationRatio
val canvas = Canvas()
canvas.drawBitmap(mBitmap, circleCenterX - SystemUtil.dp2px(context, 5),
mTouchY.toFloat() - SystemUtil.dp2px(context, 3), mWavePaint)
mViewPager?.postDelayed({
isUpdateView = true
mIsShowWave = false
invalidate()
},1000)
return false
}
mTouchY = y.toInt()
startAnimator(1.0f)
}
MotionEvent.ACTION_MOVE -> {
mIsActionDown = false
mIsShowWave = true
mTouchY = y.toInt()
if (mPreSelect != mNewSelect && mNewSelect >= 0 && mNewSelect < mLetters.size) {
mSelect = mNewSelect
if (mListener != null) {
mListener?.onLetterChange(mLetters[mNewSelect])
}
if(mSelect != -1) {
notifiChangeIndexWordBg(mLetters[mSelect])
}
}
}
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
startAnimator(0f)
mSelect = -1
mViewPager?.setNoScroll(false)
}
else -> {
}
}
return true
}
/**
* get activity's ViewPager
*/
fun getActivitysViewpager(viewPager: NoScrollViewPager){
mViewPager = viewPager
}
@SuppressLint("NewApi")
private fun startAnimator(value: Float) {
if (mRatioAnimator == null) {
mRatioAnimator = ValueAnimator()
}
mRatioAnimator?.cancel()
mRatioAnimator?.setFloatValues(value)
mRatioAnimator?.addUpdateListener { value ->
mAnimationRatio = value.animatedValue as Float
//球弹到位的时候,并且点击的位置变了,即点击的时候显示当前选择位置
if (mAnimationRatio == 1f && mPreSelect != mNewSelect) {
if (mNewSelect >= 0 && mNewSelect < mLetters.size) {
mSelect = mNewSelect
if (mListener != null) {
mListener?.onLetterChange(mLetters[mNewSelect])
}
if(mSelect != -1) {
notifiChangeIndexWordBg(mLetters[mSelect])
}
}
}
invalidate()
}
mRatioAnimator?.start()
}
fun setOnLetterChangeListener(listener: OnLetterChangeListener) {
this.mListener = listener
}
interface OnLetterChangeListener {
fun onLetterChange(letter: String)
}
}
这里需要注意的就是Adapter控制器,它需要把数据和第一个字的首字母关联起来,通过创建一个数据实体类:
/**
* 悬浮窗实体
* @author guotianhui
*/
public class TitleEntity{
private AllMemberData mValue;
private String mSortLetters;
public AllMemberData getValue() {
return mValue;
}
public void setValue(AllMemberData value) {
mValue = value;
}
public String getSortLetters() {
return mSortLetters;
}
public void setSortLetters(String sortLetters) {
mSortLetters = sortLetters;
}
@Override
public String toString() {
return "TitleEntity{" +
"mValue=" + mValue +
", mSortLetters='" + mSortLetters + '\'' +
'}';
}
}
当从网络获取到数据之后,我们就可以通过获取到数据之后,就可以把数据转换成我们需要的格式:
for (titleItem in result.data!!) {
val titleEntity = TitleEntity()
titleEntity.value = titleItem
titleEntity.sortLetters = getEnglishIndexFristWord(titleItem)
mMemberInfotList.add(titleEntity)
}
其中RecyclerView悬浮的指示条是通过自定义分割线的形式自定义出来的:
mRecyclerView.addItemDecoration (TitleItemDecoration(context))
指示条的自定义代码如下:
/**
* 所有成员列表头部的悬浮title
* @author guotianhui
*/
public class TitleItemDecoration extends RecyclerView.ItemDecoration {
private int mItemHeight;
private int mTextPadding;
private int mTextSize;
private int mTextColor;
private int mBackgroundColor;
private TextPaint mTitleTextPaint;
private Paint mBackgroundPaint;
public TitleItemDecoration(Context context) {
mItemHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 23, context.getResources().getDisplayMetrics());
mTextPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 21, context.getResources().getDisplayMetrics());
mTextSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 14, context.getResources().getDisplayMetrics());
mTextColor = Color.parseColor("#666666");
mBackgroundColor = Color.parseColor("#f4f4f4");
mTitleTextPaint = new TextPaint();
mTitleTextPaint.setAntiAlias(true);
mTitleTextPaint.setTextSize(mTextSize);
mTitleTextPaint.setColor(mTextColor);
mBackgroundPaint = new Paint();
mBackgroundPaint.setAntiAlias(true);
mBackgroundPaint.setColor(mBackgroundColor);
}
/**
* 绘制标题
*/
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDraw(c, parent, state);
if (parent.getAdapter() == null || !(parent.getAdapter() instanceof AllMemberDataAdapter)) {
return;
}
AllMemberDataAdapter adapter = (AllMemberDataAdapter) parent.getAdapter();
if (adapter.getMItemFirstWordList() == null || adapter.getMItemFirstWordList().isEmpty()) {
return;
}
for (int i = 0; i < parent.getChildCount(); i++) {
final View child = parent.getChildAt(i);
int position = parent.getChildAdapterPosition(child);
if (titleAttachView(child, parent)) {
drawTitleItem(c, parent, child, adapter.getSortLetters(position));
}
}
}
/**
* 绘制悬浮标题
*/
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
super.onDrawOver(c, parent, state);
if (parent.getAdapter() == null || !(parent.getAdapter() instanceof AllMemberDataAdapter)) {
return;
}
AllMemberDataAdapter adapter = (AllMemberDataAdapter) parent.getAdapter();
if (adapter.getMItemFirstWordList() == null || adapter.getMItemFirstWordList().isEmpty()) {
return;
}
View firstView = parent.getChildAt(0);
int firstAdapterPosition = parent.getChildAdapterPosition(firstView);
c.save();
//找到下一个标题对应的adapter position
int nextLetterAdapterPosition = adapter.getNextSortLetterPosition(firstAdapterPosition);
if (nextLetterAdapterPosition != -1) {
//下一个标题view index
int nextLettersViewIndex = nextLetterAdapterPosition - firstAdapterPosition;
if (nextLettersViewIndex < parent.getChildCount()) {
View nextLettersView = parent.getChildAt(nextLettersViewIndex);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) nextLettersView.getLayoutParams();
int nextToTop = nextLettersView.getTop() - params.bottomMargin - parent.getPaddingTop();
if (nextToTop < mItemHeight * 2) {
//有重叠
c.translate(0, nextToTop - mItemHeight * 2);
}
}
}
mBackgroundPaint.setColor(mBackgroundColor);
c.drawRect(parent.getPaddingLeft(), parent.getPaddingTop(), parent.getRight() - parent.getPaddingRight(),
parent.getPaddingTop() + mItemHeight, mBackgroundPaint);
mTitleTextPaint.setTextSize(mTextSize);
mTitleTextPaint.setColor(mTextColor);
c.drawText(adapter.getSortLetters(firstAdapterPosition),
parent.getPaddingLeft() + firstView.getPaddingLeft() + mTextPadding,
getTextBaseLineByCenter(parent.getPaddingTop() + mItemHeight / 2, mTitleTextPaint),
mTitleTextPaint);
c.restore();
}
/**
* 设置空出绘制标题的区域
*/
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
if (titleAttachView(view, parent)) {
outRect.set(0, mItemHeight, 0, 0);
} else {
super.getItemOffsets(outRect, view, parent, state);
}
}
/**
* 绘制标题信息
*/
private void drawTitleItem(Canvas c, RecyclerView parent, View child, String letters) {
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
//绘制背景
c.drawRect(parent.getPaddingLeft(), child.getTop() - params.bottomMargin - mItemHeight,
parent.getWidth() - parent.getPaddingRight(), child.getTop() - params.bottomMargin, mBackgroundPaint);
float textCenterY = child.getTop() - params.bottomMargin - mItemHeight / 2;
//绘制标题文字
c.drawText(letters, parent.getPaddingLeft() + child.getPaddingLeft() + mTextPadding,
getTextBaseLineByCenter(textCenterY, mTitleTextPaint), mTitleTextPaint);
}
public float getTextBaseLineByCenter(float center, TextPaint paint) {
Paint.FontMetrics fontMetrics = paint.getFontMetrics();
float height = fontMetrics.bottom - fontMetrics.top;
return center + height / 2 - fontMetrics.bottom;
}
/**
* 判断指定view的上方是否要空出绘制标题的位置
*
* @param view 指定的view
* @param parent 父view
*/
private boolean titleAttachView(View view, RecyclerView parent) {
if (parent.getAdapter() == null || !(parent.getAdapter() instanceof AllMemberDataAdapter)) {
return false;
}
AllMemberDataAdapter adapter = (AllMemberDataAdapter) parent.getAdapter();
if (adapter.getMItemFirstWordList() == null || adapter.getMItemFirstWordList().isEmpty()) {
return false;
}
int position = parent.getChildAdapterPosition(view);
//第一个一定要空出区域 + 每个都和前面一个去做判断,不等于前一个则要空出区域
return position == 0 ||
null != adapter.getMItemFirstWordList().get(position) && !adapter.getSortLetters(position).equals(adapter.getSortLetters(position -1));
}
}
下面就是Adapter的代码:
/**
*所有成员列表的Adapter
* @author guotianhui
*/
class AllMemberDataAdapter : BaseQuickAdapter<TitleEntity, BaseViewHolder> {
var mItemFirstWordList: ArrayList<TitleEntity>
constructor(layoutResId: Int, dataList: ArrayList<TitleEntity>) : super(layoutResId, dataList) {
mItemFirstWordList = dataList
}
fun getSortLetters(position: Int): String? {
return mItemFirstWordList[position].sortLetters
}
fun getSortLettersFirstPosition(letters: String): Int {
if (mItemFirstWordList == null || mItemFirstWordList.isEmpty()) {
return -1
}
var position = -1
for (index in mItemFirstWordList.indices) {
if (mItemFirstWordList[index].sortLetters == letters) {
position = index
break
}
}
return position
}
fun getNextSortLetterPosition(position: Int): Int {
if (mItemFirstWordList == null || mItemFirstWordList.isEmpty() || mItemFirstWordList.size <= position + 1) {
return -1
}
var resultPosition = -1
for (index in position + 1 until mItemFirstWordList.size) {
if (mItemFirstWordList[position]!= mItemFirstWordList[index]) {
resultPosition = index
break
}
}
return resultPosition
}
override fun getItemCount(): Int {
return if (mItemFirstWordList == null) 0 else mItemFirstWordList.size
}
override fun convert(helper: BaseViewHolder?, titleItem: TitleEntity?) {
val item = titleItem?.value
val headerImageView = helper?.getView<AppCompatImageView>(R.id.iv_member_header)
if(ObjectUtils.isNotEmpty(item?.studentHeadPic)) {
ImageLoader.newInstance().loadImageHeader(headerImageView, item?.studentHeadPic, R.drawable.icon_header_default)
}else{
headerImageView?.setImageResource(R.drawable.icon_header_default)
}
if(ObjectUtils.isNotEmpty(item?.nickname)) {
helper?.setText(R.id.tv_member_name, item?.nickname)
}else{
helper?.setText(R.id.tv_member_name, item?.studentName)
}
if(item?.studentGender ==1){ //1是男 2是女,0不要展示
helper?.setImageResource(R.id.iv_member_sex, R.drawable.ic_male_member)
}else{
helper?.setImageResource(R.id.iv_member_sex, R.drawable.ic_member_female)
}
if(item?.studentVIPFlag == true){
helper?.setVisible(R.id.iv_member_coach_state,true)
}else{
helper?.setVisible(R.id.iv_member_coach_state,false)
}
if(item?.studentLevel!! >=0) { //兼容小于0的情况
helper?.setText(R.id.tv_user_level, "Lv. " + item?.studentLevel.toString())
}
}
}
以上就是这个控件功能的所有代码实现。下面的是我个人的自定义控件学习心得:安卓自定义控件,除了自定义一个自定义一个ViewGroup以外,我们还可以自定义一个View,虽然都是自定义。却又有很大的不同。因为自定义ViewGroup可以理解为自定义了一个布局容器,这个布局容器的控件其实是可以自己通过Xml布局文件来定义的。但是如果你是自定义View,也就是说你直接继承自View。因为View本身就是一个父类。而且View不可以像ViewGroup一样添加一个布局。这样我们就只能通过实现View的onMeasure()、onDraw()、onLayout()方法。这个三个方法,通过字面意思我们可以知道它们的调用顺序,先是需要测量获取自定义控件的框高,然后在调用onDarw绘制。