banner
jzman

jzman

Coding、思考、自觉。
github

Custom View to Implement Alphabet Navigation Control

Today, I will share a letter navigation control for a contact list that I implemented previously. Below, I will customize a letter navigation view similar to a contact list. You need to know several elements that need to be customized, such as drawing letter indicators, drawing text, touch listening, coordinate calculation, etc. After customization, the following functions can be achieved:

  • Complete interaction between list data and letters;
  • Support for layout file attribute configuration;
  • Ability to configure related attributes in the layout file, such as letter color, letter font size, letter indicator color, etc.;

The main content is as follows:

  1. Custom attributes
  2. Measure
  3. Coordinate calculation
  4. Drawing
  5. Display effects

Custom attributes#

Create an attr.xml under the value directory and configure the custom attributes inside, as follows:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <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" />
    </declare-styleable>
</resources>

Then, obtain these attributes in the corresponding constructor and set the related attributes, as follows:

public LetterView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    //Get attributes
    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 settings
    mContext = context;
    mLetterPaint = new Paint();
    mLetterPaint.setTextSize(letterTextSize);
    mLetterPaint.setColor(letterTextColor);
    mLetterPaint.setAntiAlias(true);

    mLetterIndicatorPaint = new Paint();
    mLetterIndicatorPaint.setStyle(Paint.Style.FILL);
    mLetterIndicatorPaint.setColor(letterIndicatorColor);
    mLetterIndicatorPaint.setAntiAlias(true);

    setBackgroundColor(letterTextBackgroundColor);

    array.recycle();
}

Measure#

To accurately control the custom size and coordinates, you must measure the current custom view's width and height, and then calculate the related coordinates based on the measured size. The specific measurement process involves inheriting the View and overriding the onMeasure() method, with the key code as follows:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //Get width and height sizes
    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#

A custom view is essentially about finding the right position on the view and drawing the custom elements in order. The most challenging part of the drawing process is calculating the appropriate left position based on specific requirements. As for the drawing, it is just API calls; as long as the coordinate positions are calculated correctly, there should be no problem with the custom view drawing. The following illustration mainly marks the calculation of the center position coordinates for drawing the letter indicator and the starting position for drawing the text, ensuring that the text is centered in the indicator, as shown below:

image

Drawing#

The drawing operations for the custom view are performed in the onDraw() method. Here, we mainly use the drawing of circles and text, specifically the use of the drawCircle() and drawText() methods. To avoid the text being obscured, the letter indicator should be drawn first, followed by the letters. The code is as follows:

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

At this point, we can say that the basic drawing of the view is complete. The custom view interface can now be displayed, but the relevant event operations have not yet been added. The following will implement the relevant logic in the touch event of the view.

Touch event handling#

To determine which letter corresponds to the current finger position, you need to get the current touch coordinates to calculate the letter index. Override the onTouchEvent() method to listen for MotionEvent.ACTION_DOWN and MotionEvent.ACTION_MOVE to calculate the index position, and listen for MotionEvent.ACTION_UP to obtain the result callback, as follows:

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

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

At this point, the key parts of the custom view are basically complete.

Data assembly#

The basic idea of the letter navigation is to convert a field that needs to match with letters into corresponding letters, and then sort the data based on that field, so that data with the same initial letter can be matched through the initial letter of a data field. Here, the Chinese characters are converted to pinyin using pinyin4j-2.5.0.jar, and then the data items are sorted by their initial letters to display the data. The conversion of Chinese characters to pinyin is as follows:

//Convert Chinese characters to pinyin
public static String getChineseToPinyin(String chinese) {
    StringBuilder builder = new StringBuilder();
    HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();
    format.setCaseType(HanyuPinyinCaseType.UPPERCASE);
    format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);

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

As for data sorting, you can use the Comparator interface, which will not be elaborated here. For specific details, please refer to the source code link at the end of the article.

Display effects#

The display effect is as follows:

image

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.