安卓自定义边栏英文索引控件
Posted hyhy904
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了安卓自定义边栏英文索引控件相关的知识,希望对你有一定的参考价值。
/**
* 成员信息列表 -右侧的导航条
*/
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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
这里需要注意的就是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 + ‘\‘‘ +
‘‘;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
当从网络获取到数据之后,我们就可以通过获取到数据之后,就可以把数据转换成我们需要的格式:
for (titleItem in result.data!!)
val titleEntity = TitleEntity()
titleEntity.value = titleItem
titleEntity.sortLetters = getEnglishIndexFristWord(titleItem)
mMemberInfotList.add(titleEntity)
1
2
3
4
5
6
其中RecyclerView悬浮的指示条是通过自定义分割线的形式自定义出来的:
mRecyclerView.addItemDecoration (TitleItemDecoration(context))
1
指示条的自定义代码如下:
/**
* 所有成员列表头部的悬浮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));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
下面就是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())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
以上就是这个控件功能的所有代码实现。下面的是我个人的自定义控件学习心得:安卓自定义控件,除了自定义一个自定义一个ViewGroup以外,我们还可以自定义一个View,虽然都是自定义。却又有很大的不同。因为自定义ViewGroup可以理解为自定义了一个布局容器,这个布局容器的控件其实是可以自己通过Xml布局文件来定义的。但是如果你是自定义View,也就是说你直接继承自View。因为View本身就是一个父类。而且View不可以像ViewGroup一样添加一个布局。这样我们就只能通过实现View的onMeasure()、onDraw()、onLayout()方法。这个三个方法,通过字面意思我们可以知道它们的调用顺序,先是需要测量获取自定义控件的框高,然后在调用onDarw绘制。
---------------------
以上是关于安卓自定义边栏英文索引控件的主要内容,如果未能解决你的问题,请参考以下文章