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 functionalities that can be achieved are as follows:
- Achieve mutual interaction between list data and letters;
- Support layout file attribute configuration;
- In the layout file, relevant attributes can be configured, such as letter color, letter font size, letter indicator color, etc.;
The main content is as follows:
- Custom attributes
- Measure
- Coordinate calculation
- Drawing
- Display effects
Custom Attributes#
Create an attr.xml under the value folder 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" />
<!-- Whether to enable the indicator -->
<attr name="letterEnableIndicator" format="boolean" />
<!-- Indicator color -->
<attr name="letterIndicatorColor" format="color" />
</declare-styleable>
</resources>
Then, in the corresponding constructor method, obtain these attributes and set the relevant properties, as follows:
public LetterView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
// Obtain 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 width and height of the current custom view, and then calculate the relevant coordinates based on the measured size. The specific measurement process is to inherit the View and override the onMeasure() method to complete the measurement. The key code is as follows:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// Get the size 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#
A custom view is essentially about finding the right position on the view and drawing the custom elements in an orderly manner. The most challenging part of the drawing process is calculating the appropriate left position based on specific requirements. As for the drawing, it is all API calls; as long as the coordinate position is calculated correctly, the drawing of the custom view should not be a problem. The following illustration mainly marks the calculation of the center position coordinates for drawing the letter indicator and the starting position for drawing text. During the drawing process, ensure that the text is centered on the indicator, as referenced below:
Drawing#
The drawing operations of the custom view are all performed in the onDraw() method. Here, the main usage is the drawing of circles and text, specifically using the drawCircle() and drawText() methods. To avoid text being obscured, the letter indicator should be drawn first, followed by the letters. The code reference is as follows:
@Override
protected void onDraw(Canvas canvas) {
// Get the width and height of the letter
@SuppressLint("DrawAllocation") Rect rect = new Rect();
mLetterPaint.getTextBounds("A", 0, 1, rect);
int letterWidth = rect.width();
int letterHeight = rect.height();
// Draw the 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 the 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, we can say that the basic drawing of the view is complete. Now the custom view interface can be displayed, but related event operations have not yet been added. The following will implement the relevant logic in the touch events of the view.
Touch Event Handling#
To determine which letter corresponds to the current finger position, it is necessary to obtain the current touch coordinate position 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 referenced below:
@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;
}
So far, 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 the corresponding letters, and then sort the data according to that field, so that the data with the same initial letter can be matched through the initial letter of a certain data field. Here, the conversion of Chinese characters to pinyin uses pinyin4j-2.5.0.jar, and then sorts the data items according to the initial letter 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 source code, please refer to the link at the end of the article.
Display Effects#
The display effect is as follows: