Custom View to implement letter navigation control

Today, I share a previously implemented address book letter navigation control. Next, customize an address book like letter Navigation View. You can know several elements that need to be customized, such as drawing letter indicator, drawing text, touch monitoring, coordinate calculation, etc. the functions that can be achieved after customization are as follows:

  • Complete the interaction between list data and letters;
  • Support layout file attribute configuration;
  • Relevant properties can be configured in the layout file, such as letter color, letter font size, letter indicator color and so on.

The main contents are as follows:

  1. Custom properties
  2. Measure measurement
  3. Coordinate calculation
  4. draw
  5. Display effect

Custom properties

Create attr. Under value XML to configure the attributes to be customized, as follows:

<?xml version="1.0" encoding="utf-8"?>
    <declare-styleable name="LetterView">
        <!--Letter color-->
        <attr name="letterTextColor" format="color" />
        <!--Letter font size-->
        <attr name="letterTextSize" format="dimension" />
        <!--Overall background-->
        <attr name="letterTextBackgroundColor" format="color" />
        <!--Enable indicator-->
        <attr name="letterEnableIndicator" format="boolean" />
        <!--Indicator color-->
        <attr name="letterIndicatorColor" format="color" />

Then obtain these attributes in the corresponding construction method and set the relevant attributes, as follows:

public LetterView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    //get attribute
    TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.LetterView);
    int letterTextColor = array.getColor(R.styleable.LetterView_letterTextColor, Color.RED);
    int letterTextBackgroundColor = array.getColor(R.styleable.LetterView_letterTextBackgroundColor, Color.WHITE);
    int letterIndicatorColor = array.getColor(R.styleable.LetterView_letterIndicatorColor, Color.parseColor("#333333"));
    float letterTextSize = array.getDimension(R.styleable.LetterView_letterTextSize, 12);
    enableIndicator = array.getBoolean(R.styleable.LetterView_letterEnableIndicator, true);

    //default setting
    mContext = context;
    mLetterPaint = new Paint();

    mLetterIndicatorPaint = new Paint();



Measure measurement

To accurately control the user-defined dimensions and coordinates, the width and height of the current user-defined View must be measured, and then the relevant coordinates can be calculated through the measured dimensions. The specific measurement process is to inherit the View and rewrite the omMeasure() method to complete the measurement. The key codes are as follows:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //Gets the dimension of width and height
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    //wrap_content default width and height
    @SuppressLint("DrawAllocation") Rect mRect = new Rect();
    mLetterPaint.getTextBounds("A", 0, 1, mRect);
    mWidth = mRect.width() + dpToPx(mContext, 12);
    int mHeight = (mRect.height() + dpToPx(mContext, 5)) * letters.length;

    if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT &&
            getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
        setMeasuredDimension(mWidth, mHeight);
    } else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
        setMeasuredDimension(mWidth, heightSize);
    } else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
        setMeasuredDimension(widthSize, mHeight);

    mWidth = getMeasuredWidth();
    int averageItemHeight = getMeasuredHeight() / 28;
    int mOffset = averageItemHeight / 30; //Interface adjustment
    mItemHeight = averageItemHeight + mOffset;

Coordinate calculation

In fact, a custom View is to find a suitable position on the View and draw the customized elements in an orderly manner. The most difficult thing in the drawing process is how to calculate the appropriate left side according to specific requirements. As for drawing, it is an API call. As long as the coordinate position is calculated, there should be no problem in drawing the custom View, The following illustration mainly indicates the calculation of the coordinates of the center position of the letter indicator drawing and the calculation of the starting point position of the text drawing. During the drawing process, it is necessary to ensure that the text is in the center of the indicator. Refer to the following:

Custom letter navigation


The drawing operation of custom View is carried out in onDraw() method, which mainly uses the drawing of circle and text, specifically the use of drawCircle() and drawText() methods. In order to avoid text occlusion, it is necessary to draw letter indicator and then draw letters. The code reference is as follows:

protected void onDraw(Canvas canvas) {
    //Get letter width and height
    @SuppressLint("DrawAllocation") Rect rect = new Rect();
    mLetterPaint.getTextBounds("A", 0, 1, rect);
    int letterWidth = rect.width();
    int letterHeight = rect.height();

    //Draw indicator
    if (enableIndicator){
        for (int i = 1; i < letters.length + 1; i++) {
            if (mTouchIndex == i) {
                canvas.drawCircle(0.5f * mWidth, i * mItemHeight - 0.5f * mItemHeight, 0.5f * mItemHeight, mLetterIndicatorPaint);
    //Draw letters
    for (int i = 1; i < letters.length + 1; i++) {
        canvas.drawText(letters[i - 1], (mWidth - letterWidth) / 2, mItemHeight * i - 0.5f * mItemHeight + letterHeight / 2, mLetterPaint);

So far, it can be said that the basic drawing of View is over. Now the user-defined View interface can be displayed, but the relevant event operations have not been added. Next, the relevant logic will be implemented in the touch event of View.

Touch event handling

In order to determine which letter corresponds to the current position of the finger, you need to obtain the coordinate position of the current touch to calculate the letter index, and re onTouchEvent() method to listen to motionevent ACTION_ DOWN,MotionEvent.ACTION_MOVE to calculate the index position and listen to motionevent ACTION_ Up will call back the obtained results, as shown below:

public boolean onTouchEvent(MotionEvent event) {
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
        case MotionEvent.ACTION_MOVE:
            isTouch = true;
            int y = (int) event.getY();
            Log.i("onTouchEvent","--y->" + y + "-y-dp-->" + DensityUtil.px2dp(getContext(), y));
            int index = y / mItemHeight;

            if (index != mTouchIndex && index < 28 && index > 0) {
                mTouchIndex = index;
                Log.i("onTouchEvent","--mTouchIndex->" + mTouchIndex + "--position->" + mTouchIndex);

            if (mOnLetterChangeListener != null && mTouchIndex > 0) {
                mOnLetterChangeListener.onLetterListener(letters[mTouchIndex - 1]);

        case MotionEvent.ACTION_UP:
            isTouch = false;
            if (mOnLetterChangeListener != null && mTouchIndex > 0) {
    return true;

So far, the key part of View customization has been basically completed.

Data assembly

The basic idea of letter navigation is to convert a field that needs to be matched with letters into corresponding letters, and then sort the data according to the field, so that the data with the same initial can be matched in batches through the first letter of a data field. Pinyin4j-2.5.0 is used to convert Chinese characters into pinyin Jar, and then sort the data items according to the initial letter to display the data. The Chinese characters are replaced by Pinyin as follows:

//Convert Chinese characters to Pinyin
public static String getChineseToPinyin(String chinese) {
    StringBuilder builder = new StringBuilder();
    HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();

    char[] charArray = chinese.toCharArray();
    for (char aCharArray : charArray) {
        if (Character.isSpaceChar(aCharArray)) {
        try {
            String[] pinyinArr = PinyinHelper.toHanyuPinyinStringArray(aCharArray, format);
            if (pinyinArr != null) {
            } else {
        } catch (BadHanyuPinyinOutputFormatCombination badHanyuPinyinOutputFormatCombination) {
    return builder.toString();

As for data sorting, you can use the Comparator interface, which will not be repeated here. For details, see the source code link at the end of the article.

Display effect

The display effect is as follows:

For my inventory, please click My GitHub Free collection

Keywords: Android

Added by mguili on Sat, 15 Jan 2022 14:34:48 +0200