自定义字母导航条控件

2023-12-23 03:20

本文主要是介绍自定义字母导航条控件,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

效果图:


实现这个效果的大致步骤有:

 1. A-Z索引的绘制. 
 2. 处理Touch事件. 
 3. 提供使用监听\回调.
 4. 汉字转换成拼音.
 5. 进行排序展示. 
 6. 进行分组. 
 7. 将自定义控件和ListView合体. 


需要注意的地方有:

1.文本绘制的起始点

绘制文本是用Canvas的drawText(String text, float x, float y, Paint paint)方法,接收4个参数,分别是绘制的文本内容,文本开始绘制的x坐标,文本开始绘制的y坐标,绘制文本的画笔Paint;需要注意的是文本的绘制是以左下角为起点的.关于x,y坐标的获取分析如下图所示:


cellWidth和cellHeight分别是每一个字母所在的单元格的宽和高,由图可知,其实宽度就是该自定义控件的宽,高度就是该自定义控件的高度除以26,这个26就是英文字母的个数.

2.文本宽高的获取

由上图可知,我们是需要获取文本的宽高来计算绘制文本的x,y坐标的.那么如何计算文本的宽高呢?

可以通过Paint的getTextBounds(String text, int start, int end, Rect bounds)方法获取,接收4个参数,分别是要获取宽高信息的目标文本,从文本的第几个字符开始,到文本的第几个字符结束,用于封装文本宽高信息的矩形.这里特别注意的是第4个参数,Rect 矩形里面封装了left,top,right,bottom的信息,有了这4个值,那么获取宽高就变得很容易了.width=right-left,height=bottom-top;当然这些计算都无需我们操心了,Rect 对象已经封装好了,我们只需要传入一个空的Rect 作为getTextBounds方法的第4个参数,那么该方法就会返回一个带有宽高信息的Rect 对象给我们,然后调用Rect的height()和width()就可以获取到宽高值了.我们可以看下Rect的这2个方法的源码:

/*** @return the rectangle's width. This does not check for a valid rectangle* (i.e. left <= right) so the result may be negative.*/
public final int width() {return right - left;
}
/*** @return the rectangle's height. This does not check for a valid rectangle* (i.e. top <= bottom) so the result may be negative.*/
public final int height() {return bottom - top;
}

这里再介绍另一种方式获取文本的宽度,只是获取宽度而已,可以调用Paint的 measureText(String text)方法,返回float值,也是可以获取文本的宽度的.

3.如何获取当前选中的字母

通过重写字母导航控件的onTouchEvent方法,计算当前的event.getY(),通过该值除以字母所在单元格的高度cellHeight,就可判断当前的手指所处第几个单元格内,这样就可以获取到一个索引,通过索引就可以找到对应的英文字母了.这里需要注意的是,需要排除手指在同一个单元格内不断触摸的情况,否则会造成通过字母索引多次回调,说到回调,这里我们是需要自定义接口保留方法,通过接口回调的方式暴露当前用户所选择的字母的.

4.数据的排序

值得一提的是,我们在ListView中展示的数据是经过排序,也就是说拼音首字母相同的中文是要归为同一组显示的,当然如果是英文字母的话就更好了,都不需要经过中文转拼音,再截取拼音首字母的过程操作了.那么排序如何实现呢,其实就是比较拼音字符串的自然顺序而已,目标bean需要实现Comparable接口,然后实现其compareTo方法,在该方法内部进行字符串的自然顺序的比较即可.

5.中文如何转字符串

这个可以借助一些三方jar包就可以实现了,本例采用的是pinyin4j-2.5.0.jar.具体实现看如下工具类:

/*** Created by mChenys on 2015/12/20.*/
public class PingYingUtils {/*** 根据传入的字符串(包含汉字),得到拼音* 拼音 -> PINGYING* 拼  音*& -> PINGYING*& 保留特殊符号,却掉空格** @param str 字符串* @return*/public static String getPinyin(String str) {//拼音的格式HanyuPinyinOutputFormat format = new HanyuPinyinOutputFormat();format.setCaseType(HanyuPinyinCaseType.UPPERCASE); //全部大写format.setToneType(HanyuPinyinToneType.WITHOUT_TONE); //去掉声调StringBuilder sb = new StringBuilder();char[] charArray = str.toCharArray();for (int i = 0; i < charArray.length; i++) {char c = charArray[i];// 如果是空格, 跳过if (Character.isWhitespace(c)) {continue;}if (c >= -127 && c < 128) {// 肯定不是汉字,保留特殊字符和英文字母sb.append(c);} else {String s = "";try {// 通过char得到拼音集合,因为有多音字的存在,这里不考虑,直接取第一个匹配的拼音s = PinyinHelper.toHanyuPinyinStringArray(c, format)[0];sb.append(s);} catch (BadHanyuPinyinOutputFormatCombination e) {e.printStackTrace();sb.append(s);}}}return sb.toString();}
}


6.如何控制ListView中Item显示字母title控件的时候所有同一组的只显示一次呢?

也就是下图这个,这个是如何做到的呢?


要控制这个只显示一次,需要在ListView的Adapter内做控制,在getView方法中需要从数据Bean中分别获取索引为position和position-1时的拼音首字母,然后对比是否是一样的,如果是一样的就说明是同一个组,如果是同一组那么就不显示字母title控件,否则就显示,通过View的setVisibility(int visibility)方法来实现.


好了分析了理论,现在来看代码吧

1.自定义控件MyQuickIndexBar 

/*** Created by mChenys on 2015/12/20.*/
public class MyQuickIndexBar extends View {private Paint mPaint;//绘制文本的画笔private char[] mLetters = new char[26];//存放26个字母的字符数组private int mCellWidth; //文本所在矩形区域的宽度private float mCellHeight;//文本所在矩形区域的高度public MyQuickIndexBar(Context context) {this(context, null);}public MyQuickIndexBar(Context context, AttributeSet attrs) {this(context, attrs, 0);}public MyQuickIndexBar(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init();}private void init() {//初始化画笔mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);mPaint.setColor(Color.parseColor("#8B8B8B"));mPaint.setTypeface(Typeface.DEFAULT_BOLD);mPaint.setTextSize(25);//初始化26个单词字母for (int i = 0; i < 26; i++) {mLetters[i] = (char) ('A' + i);}}//控件大小发生变化时回调@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {mCellWidth = getMeasuredWidth();mCellHeight = getMeasuredHeight() * 1.0f / mLetters.length;}@Overrideprotected void onDraw(Canvas canvas) {//确定文本的绘制的开始坐标,文本绘制的坐标是以左下角为起点的.for (int i = 0; i < mLetters.length; i++) {String text = String.valueOf(mLetters[i]);//获取文本的宽度和高度Rect bounds = new Rect();//创建一个空矩形区域mPaint.getTextBounds(text, 0, text.length(), bounds);//将文本绘制在空的矩区域中float textHeight = bounds.height();//从矩形区域中获取文本的高度float textWidth = bounds.width(); //也可以用mPaint.measureText(text);获取宽度//计算文本的左下角x和y坐标//x坐标:文本所在单元格宽度/2 - 文本宽度/2int x = (int) (mCellWidth / 2.0f - textWidth / 2.0f);//y坐标:文本所在单元格的高度/2+文本高度/2 + 文本之间的间距(相邻2个文本的间距刚好是一个单元格的高度)int y = (int) (mCellHeight / 2.0f + textHeight / 2.0f + i * mCellHeight);//把选中的文本高亮显示mPaint.setColor(selectIndex == i ? Color.parseColor("#77D1F3") : Color.parseColor("#8B8B8B"));//绘制文本canvas.drawText(text, x, y, mPaint);}}private int lastIndex = -1; //上一次选择的位置private int selectIndex = -1;  //当前选择的位置@Overridepublic boolean onTouchEvent(MotionEvent event) {switch (MotionEventCompat.getActionMasked(event)) {case MotionEvent.ACTION_MOVE:case MotionEvent.ACTION_DOWN:// 获取当前触摸到的字母索引selectIndex = (int) (event.getY() / mCellHeight);if (selectIndex >= 0 && selectIndex < mLetters.length) {// 判断是否跟上一次触摸到的一样,避免在同一个单元格内触摸时多次回调if (selectIndex != lastIndex) {lastIndex = selectIndex;if (null != changeCallback) {changeCallback.onChange(String.valueOf(mLetters[selectIndex]), selectIndex);}}}break;case MotionEvent.ACTION_UP:lastIndex = -1;break;default:break;}//不断回调onDraw方法invalidate();return true;}//字母改变的回到接口public interface OnWordsChangeCallback {void onChange(String word, int index);}private OnWordsChangeCallback changeCallback;public void setOnWordsChangeCallback(OnWordsChangeCallback changeCallback) {this.changeCallback = changeCallback;}
}


2.数据Bean

/*** Created by mChenys on 2015/12/20.*/
public class Person implements Comparable<Person> {public String name; //人名public String pingYing; //拼音public Person(String name) {this.name = name;this.pingYing = PingYingUtils.getPinyin(name);}@Overridepublic String toString() {return "Person{" +"name='" + name + '\'' +", pingYing='" + pingYing + '\'' +'}';}/*** 获取数据集合** @return*/public static List<Person> getPersons() {List<Person> list = new ArrayList<>();for (int i = 0; i < Constant.NAME.length; i++) {list.add(new Person(Constant.NAME[i]));}//排序Collections.sort(list);return list;}//根据拼音排序@Overridepublic int compareTo(Person another) {return this.pingYing.compareTo(another.pingYing);}
}


3.数据源,也就是一个字符串数组,由于数据太多,就直接贴图了,图只截了部分.


4.测试类MainActivity

public class MainActivity extends AppCompatActivity {private MyQuickIndexBar mQuickIndexBar; //自定义的字母导航条private TextView mIndicatorView; //中间显示的提示文本private ListView mListView;private List<Person> mData = new ArrayList<>(); //数据集合@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);initData();initView();}/*** 初始数据*/private void initData() {mData = Person.getPersons();System.out.println(mData);}/*** 初始化View*/private void initView() {mQuickIndexBar = (MyQuickIndexBar) findViewById(R.id.quickIndexBar);mIndicatorView = (TextView) findViewById(R.id.indicatorView);mIndicatorView.setVisibility(View.GONE);//导航条的字母改变监听mQuickIndexBar.setOnWordsChangeCallback(new MyQuickIndexBar.OnWordsChangeCallback() {@Overridepublic void onChange(String word, int index) {//显示提示showIndicatorView(word);//滚动ListView到选中的字母索引位置showListViewSelected(word);}});mListView = (ListView) findViewById(R.id.listView);mListView.setAdapter(adapter);}/*** 显示用户点击的字母索引*/private Handler mHandler = new Handler();private void showIndicatorView(String word) {mIndicatorView.setVisibility(View.VISIBLE);mIndicatorView.setText(word);mHandler.removeCallbacksAndMessages(null);mHandler.postDelayed(new Runnable() {@Overridepublic void run() {mIndicatorView.setVisibility(View.GONE);}},2000);}/*** 滚动ListView到选中的字母索引位置** @param word*/private void showListViewSelected(String word) {for (int i = 0; i < mData.size(); i++) {Person person = mData.get(i);String target = person.pingYing.charAt(0) + "";if (TextUtils.equals(word, target)) {// 匹配成功mListView.setSelection(i);break; //匹配成功记得跳出循环}}}//适配器private BaseAdapter adapter = new BaseAdapter() {@Overridepublic int getCount() {return mData.size();}@Overridepublic String getItem(int position) {return mData.get(position).name;}@Overridepublic long getItemId(int position) {return position;}@Overridepublic View getView(int position, View convertView, ViewGroup parent) {ViewHolder holder = null;if (convertView == null) {holder = new ViewHolder();convertView = View.inflate(MainActivity.this, R.layout.item_list, null);convertView.setTag(holder);holder.tvName = (TextView) convertView.findViewById(R.id.tv_name);holder.tvGroupName = (TextView) convertView.findViewById(R.id.tv_groupName);holder.llGroupBar = (LinearLayout) convertView.findViewById(R.id.ll_groupBar);} else {holder = (ViewHolder) convertView.getTag();}//显示人名holder.tvName.setText(getItem(position));//分组操作,如果人名的首字母相同则归为一类,只显示一个字母title//判断原理就是取当前的条目的拼音首字母和上一个条目的首字母对比,如果相同则是同一组String currIndexWord = String.valueOf(mData.get(position).pingYing.charAt(0));boolean isSameGroup = false;if (position > 0) {//和上一个条目的拼音首字母作对比String lastIndexWord = String.valueOf(mData.get(position - 1).pingYing.charAt(0));if (lastIndexWord.equals(currIndexWord)) {isSameGroup = true;}}//显示拼音首字母的title横幅,如果是不同组则显示该组的横幅holder.llGroupBar.setVisibility(!isSameGroup ? View.VISIBLE : View.GONE);holder.tvGroupName.setText(currIndexWord);return convertView;}class ViewHolder {TextView tvName, tvGroupName;LinearLayout llGroupBar;}};
}


5.activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:background="#EEEEEE"tools:context="mchenys.net.csdn.blog.myquickindexbar.MainActivity"><!--listview--><ListViewandroid:id="@+id/listView"android:scrollbars="none"android:layout_toLeftOf="@+id/quickIndexBar"android:layout_width="match_parent"android:layout_height="match_parent" /><!--字母导航控件--><mchenys.net.csdn.blog.myquickindexbar.view.MyQuickIndexBarandroid:id="@+id/quickIndexBar"android:layout_width="30dp"android:layout_height="match_parent"android:layout_alignParentRight="true"/><!--屏幕正中显示当前选中的字母的提示框--><TextViewandroid:id="@+id/indicatorView"android:layout_width="80dp"android:layout_height="80dp"android:layout_centerInParent="true"android:background="@drawable/letter_indicator_shape"android:gravity="center"android:textColor="@android:color/white"android:textSize="30sp"android:textStyle="bold" />
</RelativeLayout>

6.ListView的Item布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><!--显示字母的title横幅--><LinearLayoutandroid:id="@+id/ll_groupBar"android:layout_width="match_parent"android:layout_height="wrap_content"android:orientation="vertical"><TextViewandroid:id="@+id/tv_groupName"android:layout_width="match_parent"android:layout_height="40dp"android:gravity="center_vertical"android:paddingLeft="10dp"android:text="A"android:textColor="@android:color/holo_blue_light"android:textSize="20sp"android:textStyle="bold" /><Viewandroid:layout_width="match_parent"android:layout_height="2dp"android:background="@android:color/holo_blue_light" /></LinearLayout><!--显示文本--><TextViewandroid:id="@+id/tv_name"android:layout_width="match_parent"android:layout_height="60dp"android:gravity="center_vertical"android:paddingLeft="10dp"android:text="玉帝"android:textColor="@android:color/black"android:textSize="20sp"android:textStyle="bold" />
</LinearLayout>


 



这篇关于自定义字母导航条控件的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/526521

相关文章

C#实现WinForm控件焦点的获取与失去

《C#实现WinForm控件焦点的获取与失去》在一个数据输入表单中,当用户从一个文本框切换到另一个文本框时,需要准确地判断焦点的转移,以便进行数据验证、提示信息显示等操作,本文将探讨Winform控件... 目录前言获取焦点改变TabIndex属性值调用Focus方法失去焦点总结最后前言在一个数据输入表单

SpringBoot 自定义消息转换器使用详解

《SpringBoot自定义消息转换器使用详解》本文详细介绍了SpringBoot消息转换器的知识,并通过案例操作演示了如何进行自定义消息转换器的定制开发和使用,感兴趣的朋友一起看看吧... 目录一、前言二、SpringBoot 内容协商介绍2.1 什么是内容协商2.2 内容协商机制深入理解2.2.1 内容

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

usaco 1.2 Name That Number(数字字母转化)

巧妙的利用code[b[0]-'A'] 将字符ABC...Z转换为数字 需要注意的是重新开一个数组 c [ ] 存储字符串 应人为的在末尾附上 ‘ \ 0 ’ 详见代码: /*ID: who jayLANG: C++TASK: namenum*/#include<stdio.h>#include<string.h>int main(){FILE *fin = fopen (

自定义类型:结构体(续)

目录 一. 结构体的内存对齐 1.1 为什么存在内存对齐? 1.2 修改默认对齐数 二. 结构体传参 三. 结构体实现位段 一. 结构体的内存对齐 在前面的文章里我们已经讲过一部分的内存对齐的知识,并举出了两个例子,我们再举出两个例子继续说明: struct S3{double a;int b;char c;};int mian(){printf("%zd\n",s

Spring 源码解读:自定义实现Bean定义的注册与解析

引言 在Spring框架中,Bean的注册与解析是整个依赖注入流程的核心步骤。通过Bean定义,Spring容器知道如何创建、配置和管理每个Bean实例。本篇文章将通过实现一个简化版的Bean定义注册与解析机制,帮助你理解Spring框架背后的设计逻辑。我们还将对比Spring中的BeanDefinition和BeanDefinitionRegistry,以全面掌握Bean注册和解析的核心原理。

Oracle type (自定义类型的使用)

oracle - type   type定义: oracle中自定义数据类型 oracle中有基本的数据类型,如number,varchar2,date,numeric,float....但有时候我们需要特殊的格式, 如将name定义为(firstname,lastname)的形式,我们想把这个作为一个表的一列看待,这时候就要我们自己定义一个数据类型 格式 :create or repla

lvgl8.3.6 控件垂直布局 label控件在image控件的下方显示

在使用 LVGL 8.3.6 创建一个垂直布局,其中 label 控件位于 image 控件下方,你可以使用 lv_obj_set_flex_flow 来设置布局为垂直,并确保 label 控件在 image 控件后添加。这里是如何步骤性地实现它的一个基本示例: 创建父容器:首先创建一个容器对象,该对象将作为布局的基础。设置容器为垂直布局:使用 lv_obj_set_flex_flow 设置容器

HTML5自定义属性对象Dataset

原文转自HTML5自定义属性对象Dataset简介 一、html5 自定义属性介绍 之前翻译的“你必须知道的28个HTML5特征、窍门和技术”一文中对于HTML5中自定义合法属性data-已经做过些介绍,就是在HTML5中我们可以使用data-前缀设置我们需要的自定义属性,来进行一些数据的存放,例如我们要在一个文字按钮上存放相对应的id: <a href="javascript:" d

一步一步将PlantUML类图导出为自定义格式的XMI文件

一步一步将PlantUML类图导出为自定义格式的XMI文件 说明: 首次发表日期:2024-09-08PlantUML官网: https://plantuml.com/zh/PlantUML命令行文档: https://plantuml.com/zh/command-line#6a26f548831e6a8cPlantUML XMI文档: https://plantuml.com/zh/xmi