这个最近在做一个相关的通讯录索引显示,网上大都是手机竖屏的显示但是对于平板的横屏显示的基本上没有。但是原理基本上是一直的,就是自定义View一个索引控件,这里的改编自网上的一个手机通讯录索引项目。本身这里的demo也是改编于它的项目。
该demo实现的相关相关功能,水平布局显示相关联系人头像和名字。添加每个项目的首字母显示。点击下方索引跳转到对应的联系人部分。下方索引可以随滑动显示并变更布局。由于是demo项目相关编码比较随便。
这个就是效果图。
这里涉及到对象是有首字母拼音的,这里采用相关的tinypinyin 架构相关的联系人对象可以自己处理,这里的对象中要包含相关的首字母并涉及 到相关是否显示。
CharLoadIndexView 这个就是下方的底部索引自定义view。而CharIndexView这个则是相关的右部侧索引。
相关的构造方法和初始化就不介绍了,可以看代码。这里从相关的OnLayout方法介绍。
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Paint.FontMetrics fm = textPaint.getFontMetrics();
float textHeight = fm.bottom - fm.top;
int width = getWidth() -getPaddingLeft() - getPaddingRight();
itemWeight = width/(float)CHARS.length;
textY = textHeight;
textX = itemWeight;
这里对于每个索引字项找到对应的高度和宽度。这里需要注意的是,底部排列则是屏幕宽度除相关的索引数组长度,侧边排列则是高度除以长度。
具体的绘制方法则是在onDraw()方法中:
@Override
protected void onDraw(Canvas canvas) {
float centerX = getPaddingLeft() +textX/2.0f;
float centerY =getPaddingTop() + textY/2.0f;
if (centerX <= 0 || centerY <= 0) return;
for (int i = 0; i < CHARS.length; i++) {
char c = CHARS[i];
textPaint.setColor(i == currentIndex ? indexTextColor : textColor);
canvas.drawText(String.valueOf(c), centerX, centerY, textPaint);
centerX += itemWeight;
}
}
由于是从左边和顶部开始绘制。则需要考虑相关的位置,这里座标中心,可以计算出为相对应的高度的1/2,至于为什么看座标参考系应该明白。这里由于是底部显示,则绘制完成一个字段之后,相关的横座标X则增加一个单字段的宽度。
这里相关的字体颜色设置 ,可以自己根据实际情况变化,这里下方的触摸事件就改变相关的位置,从而达到改变对应的索引颜色。
@Override
public boolean onTouchEvent(MotionEvent event) {
Log.d(TAG, "onTouchEvent: " + event.getAction());
int currentIndex = INDEX_NONE;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
setBackgroundDrawable(indexDrawable);
currentIndex = computeCurrentIndex(event);
if (listener != null) {
listener.onCharIndexSelected(String.valueOf(CHARS[currentIndex]));
}
break;
case MotionEvent.ACTION_MOVE:
currentIndex = computeCurrentIndex(event);
if (listener != null) {
listener.onCharIndexSelected(String.valueOf(CHARS[currentIndex]));
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
setBackgroundDrawable(null);
if (listener != null) {
listener.onCharIndexSelected(null);
}
break;
}
if (currentIndex != this.currentIndex) {
this.currentIndex = currentIndex; //这控制相关的点击的字母颜色
invalidate();
if (this.currentIndex != INDEX_NONE && listener != null) {
listener.onCharIndexChanged(CHARS[this.currentIndex]);
}
}
return true;
}
触摸事件主要是触摸相关的监听的接口事件以便于注册接口的地方调用,以及改变对应的下方索引颜色。
private int computeCurrentIndex(MotionEvent event) {
if (itemWeight <= 0) return INDEX_NONE;
float x = event.getX() - getPaddingLeft();
int index = (int) (x / itemWeight);
if (index < 0) {
index = 0;
} else if (index >= CHARS.length) {
index = CHARS.length - 1;
}
return index;
}
这里根据事件得到相关的索引位置,如果是侧边则应该使用相关的高度Y值来做判断。
当然还有一个重要的功能就是在相关的每个首字母第一项显示对应的字母分组。这里采用了网上的方案就是使用分割线。
通过查看官方文档,:addItemDecoration(RecyclerView.ItemDecoration decor)这个方法设置分隔线RecyclerView.ItemDecoration是一个类,里面封装了三个方法:
(1)void getItemOffsets ()
(2)void onDraw ()
(3)void onDrawOver ()
通过上面的三个方法,可以看出,这是要自己直接画上去,准确的说这几个方法是:添加Divider,主要是找到添加Divider的位置, 而Divider是在drawable文件中写好了的。 利用onDraw和onDrawOver都差不多,我们在创建自己的Decoration类继承RecyclerView.ItemDecoration的时候,我们只要重写getItemOffsets(),还有onDraw()和onDrawOver两者其中之一就可以了.
那getItemOffsets()方法有什么用呢?从字面意思就是Item要偏移, 由于我们在Item和Item之间加入了分隔线,线其实本质就是一个长方形,也是用户自定义的,既然线也有长宽高,就画横线来说,上面的Item加入了分隔线,那下面的Item就要往下平移,平移的量就是分隔线的高度。
我们要确定:分隔线的left, top, right, Bottom. 在Adapter中,我们很容易通过parent(这个parent它其实就是我们能看到的部分)获取每一个childView:
(1)left:parent.getPaddingLeft()
(2)right: parent. getWidth()-parent.getPaddingRight();
(3)top : 就是红线的上面:我们通过ChildView.getBottom()来得到这个Item的底部的高度,也就是蓝线位置,蓝线和红线之间间距:就是这个Item布局文件的:layout_marginBottom, 然后top的位置就是两者之和。
(4)bttom: 就是top加上分隔线的高度:top+线高
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State
state) {
int position = parent.getChildAdapterPosition(view);
int headerHeight = 0;
if (position != RecyclerView.NO_POSITION
&& hasHeader(position) && shouldShowHeader(position)) {
headerHeight = getHeader(parent, position).itemView.getHeight();
}
outRect.set(0, headerHeight, 0, 0);
}
这个就是测算每个item应该偏移的位置。
private long firstHeaderId = 0; //每次的的真正第一行显示
/**
* {@inheritDoc}
*/
@Override
public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) {
final int count = parent.getChildCount();
for (int layoutPos = 0; layoutPos < count; layoutPos++) {
final View child = parent.getChildAt(layoutPos);
final int adapterPos = parent.getChildAdapterPosition(child);
if (adapterPos != RecyclerView.NO_POSITION && hasHeader(adapterPos)) {
long headerId = mAdapter.getHeaderId(adapterPos);
if (headerId != previousHeaderId ) {
View header = getHeader(parent, adapterPos).itemView;
canvas.save();
final int left = child.getLeft();
final int top = getHeaderTop(parent, child, header, adapterPos, layoutPos);
canvas.translate(left, top);
header.setTranslationX(left);
header.setTranslationY(top);
header.draw(canvas);
canvas.restore();
previousHeaderId = headerId;
}
}
}
}
这里怎么控制每次的header是否显示呢,当每次绘制的时候如果这个头部和索引的记录值不相等就绘制一次。
怎个Demo就介绍到这里,这个demo中还使用到了相关的Rxjava2 处理对应的数据获取和显示数据的线程切换。
相关项目源码demo地址:https://github.com/tangrunfa/BotIndexContact