安卓自定义边栏英文索引控件

/**
 * 成员信息列表 -右侧的导航条
 */
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绘制。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值