先看效果图,右侧字母导航栏滑动显示,列表中的字母item滑动悬停效果
附有下载链接,可运行:https://download.csdn.net/download/qiushuiduren/89962833?spm=1001.2014.3001.5503
项目需要引入依赖
implementation 'com.github.promeg:tinypinyin:1.0.0'
主页面布局文件也很简单
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 列表,占满屏幕 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:overScrollMode="never"
android:scrollbars="none" />
<!-- 固定头item,浮在recyclerview上层的顶部 -->
<include layout="@layout/header" />
<!-- 中间字母提示控件,浮在recyclerview上层的右侧 -->
<com.contacts.demo.view.CenterTipView
android:id="@+id/tv_center_tip"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_centerInParent="true"
android:gravity="center"
android:visibility="gone"
app:bgColor="#90808080"
app:textColor="#FF0080FF"
app:textSize="60sp"
app:type="round" />
<!-- 右侧索引控件,浮在recyclerview上层的中心 -->
<com.contacts.demo.view.RightIndexView
android:id="@+id/vg_right_container"
android:layout_width="25dp"
android:layout_height="500dp"
android:layout_alignParentRight="true"
android:layout_marginEnd="12dp"
android:layout_marginTop="80dp"
app:itemTextColor="#151515"
app:itemTextSize="4sp"
app:itemTextTouchBgColor="@android:color/white"
app:itemTouchBgColor="#151515"
app:rootBgColor="#11000000"
app:rootTouchBgColor="#25000000" />
</RelativeLayout>
主要实现逻辑就是自定义右侧的字母导航条(RightIndexView,也可以用RecyclerView或者ListView实现)
public class RightIndexView extends ViewGroup {
private Context mContext;
private ArrayList<String> list = new ArrayList<>();
//自定义属性(item的背景默认透明)
private int rootBgColor;
private int rootTouchBgColor;
private int itemTouchBgColor;
private int itemTextColor;
private int itemTextTouchBgColor;
private int itemTextSize;
//记录上一次touch的位置
private int mOldViewIndex;
//root的宽高
private int mWidth;
private int mHeight;
//需要添加进来的item给定的高度
private int mItemHeight;
//root上边和下边的内间距
private int marginTop = 5;
private int marginBottom = 5;
//touch move最小距离
private int mTouchSlop;
//记录touch的down与move位置,以便计算移动的距离
private int yDown;
private int yMove;
//touch监听
private OnRightTouchMoveListener onRightTouchMoveListener;
public void setOnRightTouchMoveListener(OnRightTouchMoveListener onRightTouchMoveListener) {
this.onRightTouchMoveListener = onRightTouchMoveListener;
}
public RightIndexView(Context context) {
this(context, null);
}
public RightIndexView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RightIndexView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
//获取自定义属性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RightIndexView);
int count = typedArray.getIndexCount();
for (int i = 0; i < count; i++) {
int attr = typedArray.getIndex(i);
switch (attr) {
case R.styleable.RightIndexView_rootBgColor:
//容器的背景颜色,没有则使用指定的默认值
rootBgColor = typedArray.getColor(attr, Color.parseColor("#80808080"));
break;
case R.styleable.RightIndexView_rootTouchBgColor:
//容器touch时的背景颜色
rootTouchBgColor = typedArray.getColor(attr, Color.parseColor("#EE808080"));
break;
case R.styleable.RightIndexView_itemTouchBgColor:
//item项的touch时背景颜色(item的背景默认透明)
itemTouchBgColor = typedArray.getColor(attr, Color.parseColor("#000000"));
break;
case R.styleable.RightIndexView_itemTextColor:
//item的文本颜色
itemTextColor = typedArray.getColor(attr, Color.parseColor("#FFFFFF"));
break;
case R.styleable.RightIndexView_itemTextTouchBgColor:
//item在touch时的文本颜色
itemTextTouchBgColor = typedArray.getColor(attr, Color.parseColor("#FF0000"));
break;
case R.styleable.RightIndexView_itemTextSize:
//item的文本字体(默认16)
itemTextSize = typedArray.getDimensionPixelSize(attr,
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
break;
}
}
//回收属性数组
typedArray.recycle();
this.mContext = context;
//设置容器默认背景
setBackgroundColor(rootBgColor);
//获取系统指定的最小move距离
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
/**
* 测量子view和自己的大小
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//获取系统测量的参数
mWidth = MeasureSpec.getSize(widthMeasureSpec);
mHeight = MeasureSpec.getSize(heightMeasureSpec);
//第一个元素的top位置
int top = 5;
//item个数
int size = 0;
if (list != null && list.size() > 0) {
//获取子孩子个数
size = list.size();
//上下各减去5px,除以个数计算出每个item的应有height
mItemHeight = (mHeight - marginTop - marginBottom) / size;
}
/**
* 此循环只是测量计算textview的上下左右位置数值,保存在其layoutParams中
*/
for (int i = 0; i < size; i++) {
TextView textView = (TextView) getChildAt(i);
LayoutParams layoutParams = (LayoutParams) textView.getLayoutParams();
layoutParams.height = mItemHeight;//每个item指定应有的高度
layoutParams.width = mWidth;//宽度为容器宽度
layoutParams.top = top;//第一个item距上边5px
top += mItemHeight;//往后每个item距上边+mItemHeight距离
}
/**
* 由于此例特殊,宽度指定固定值,高度也是占满屏幕,不存在wrap_content情况
* 故不需要根据子孩子的宽高来改动 mWidth 和 mHeight 的值。故最后直接保存初始计算的值
*/
setMeasuredDimension(mWidth, mHeight);
}
/**
* 根据计算的子孩子值,在容器中布局排版子孩子
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//获取子孩子个数
int size = getChildCount();
for (int i = 0; i < size; i++) {
//得到特性顺序的子孩子
TextView textView = (TextView) getChildAt(i);
//拿到孩子中保存的数据
LayoutParams layoutParams = (LayoutParams) textView.getLayoutParams();
//给子孩子布局位置(左,上,右,下)
textView.layout(0, layoutParams.top, layoutParams.width, layoutParams.top + layoutParams.height);
}
/**
* 结束了 onMeasure 和 onLayout 之后,当前容器的职责完成,onDraw 由子孩子自己画
*/
}
public void setData(ArrayList<String> list) {
if (list == null || list.size() <= 0)
return;
int size = list.size();
this.list.addAll(list);
for (int i = 0; i < size; i++) {
addView(list.get(i), i);
}
//requestLayout();//重新measure和layout
//invalidate();//重新draw
//postInvalidate();//重新draw
}
private void addView(String firstPinYin, int position) {
TextView textView = new TextView(mContext);
textView.setText(firstPinYin);
textView.setBackgroundColor(Color.TRANSPARENT);
textView.setTextColor(itemTextColor);
textView.setTextSize(itemTextSize);
textView.setGravity(Gravity.CENTER);
textView.setTag(position);
addView(textView, position);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//当前手指位置的y坐标
int y = (int) event.getY();
//根据当前的y计算当前所在child的索引位置
int tempIndex = computeViewIndexByY(y);
if (tempIndex != -1) {
//两头我留了点距离,不等于-1就代表没出界
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
yDown = y;
drawTextView(mOldViewIndex, false);
drawTextView(tempIndex, true);
mOldViewIndex = tempIndex;
if (onRightTouchMoveListener != null) {
onRightTouchMoveListener.showTip(tempIndex, ((TextView) getChildAt(tempIndex)).getText().toString(), true);
}
//设置root touch bg
setBackgroundColor(rootTouchBgColor);
break;
case MotionEvent.ACTION_MOVE:
yMove = y;
int distance = yDown - yMove;
if (Math.abs(distance) > mTouchSlop) {
//移动距离超出了一定范围
if (mOldViewIndex != tempIndex) {
//移动超出了当前元素
drawTextView(mOldViewIndex, false);
drawTextView(tempIndex, true);
mOldViewIndex = tempIndex;
setBackgroundColor(rootTouchBgColor);
if (onRightTouchMoveListener != null) {
onRightTouchMoveListener.showTip(tempIndex, ((TextView) getChildAt(tempIndex)).getText().toString(), true);
}
}
}
break;
case MotionEvent.ACTION_UP:
drawTextView(mOldViewIndex, false);
drawTextView(tempIndex, false);
mOldViewIndex = tempIndex;
setBackgroundColor(rootBgColor);
if (onRightTouchMoveListener != null) {
onRightTouchMoveListener.showTip(tempIndex, ((TextView) getChildAt(tempIndex)).getText().toString(), false);
}
break;
}
} else {
//出界了,可能是上边或者下边出界,恢复上边的元素
if (list != null && list.size() > 0) {
drawTextView(mOldViewIndex, false);
setBackgroundColor(rootBgColor);
if (onRightTouchMoveListener != null) {
onRightTouchMoveListener.showTip(mOldViewIndex, ((TextView) getChildAt(mOldViewIndex)).getText().toString(), false);
}
}
}
return true;
}
/**
* 依据y坐标、子孩子的高度和容器总高度计算当前textview的索引值
*/
private int computeViewIndexByY(int y) {
int returnValue;
if (y < marginTop || y > (marginTop + mItemHeight * list.size())) {
returnValue = -1;
} else {
int times = (y - marginTop) / mItemHeight;
int remainder = (y - marginTop) % mItemHeight;
if (remainder == 0) {
returnValue = --times;
} else {
returnValue = times;
}
}
return returnValue;
}
/**
* 修改右边索引处touch的样式
*
* @param index position
* @param isDrawStyle 是否设置特有的色彩样式
*/
private void drawTextView(int index, boolean isDrawStyle) {
if (index < 0 || index >= list.size())
return;
TextView textView = (TextView) getChildAt(index);
if (textView == null)
return;
if (isDrawStyle) {
textView.setBackgroundColor(itemTouchBgColor);
textView.setTextColor(itemTextTouchBgColor);
} else {
textView.setBackgroundColor(Color.TRANSPARENT);
textView.setTextColor(itemTextColor);
}
}
public interface OnRightTouchMoveListener {
void showTip(int position, String content, boolean isShow);
}
/**
* 必须重写的方法
*
* @return
*/
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
public static class LayoutParams extends ViewGroup.LayoutParams {
public int left;
public int top;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
}
联系人列表的适配器如下
public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ContactViewHolder> {
public static final int SHOW_HEADER_VIEW = 1; //显示header
public static final int DISMISS_HEADER_VIEW = 2;//隐藏header
private ArrayList<Contact> list = new ArrayList<>();
private OnItemClickListener onItemClickListener;
public ContactAdapter(ArrayList<Contact> list) {
this.list = list;
}
public void updateData(ArrayList<Contact> list) {
this.list = list;
notifyDataSetChanged();
}
public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
this.onItemClickListener = onItemClickListener;
}
@Override
public ContactViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new ContactViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item, parent, false));
}
@Override
public void onBindViewHolder(final ContactViewHolder holder, final int position) {
if (list == null || list.size() <= 0)
return;
final Contact contact = list.get(position);
holder.tvHeader.setText(contact.firstPinYin);
holder.tvName.setText(contact.name);
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
onItemClickListener.onItemClick(holder.getLayoutPosition(), contact);
}
});
if (position == 0) {
holder.tvHeader.setText(contact.firstPinYin);
holder.tvHeader.setVisibility(View.VISIBLE);
} else {
if (!TextUtils.equals(contact.firstPinYin, list.get(position - 1).firstPinYin)) {
holder.tvHeader.setVisibility(View.VISIBLE);
holder.tvHeader.setText(contact.firstPinYin);
holder.itemView.setTag(SHOW_HEADER_VIEW);
} else {
holder.tvHeader.setVisibility(View.GONE);
holder.itemView.setTag(DISMISS_HEADER_VIEW);
}
}
holder.itemView.setContentDescription(contact.firstPinYin);
}
@Override
public int getItemCount() {
return (list == null || list.size() <= 0) ? 0 : list.size();
}
public static class ContactViewHolder extends RecyclerView.ViewHolder {
public TextView tvHeader;
public TextView tvName;
public ContactViewHolder(View itemView) {
super(itemView);
tvHeader = (TextView) itemView.findViewById(R.id.tv_header);
tvName = (TextView) itemView.findViewById(R.id.tv_name);
}
}
public interface OnItemClickListener {
void onItemClick(int position, Contact contact);
}
}
MainActivity的代码如下
public class MainActivity extends AppCompatActivity implements RightIndexView.OnRightTouchMoveListener {
private ArrayList<Contact> list = new ArrayList<>();//列表展示的数据
private ArrayList<String> firstList = new ArrayList<>();//字母索引集合
private HashSet<String> set = new HashSet<>();//中间临时集合
private ContactAdapter adapter;
private LinearLayoutManager layoutManager;
private RecyclerView recyclerView;
private TextView tvHeader;//固定头view
private CenterTipView tipView;//中间字母提示view
private RightIndexView rightContainer;//右侧索引view
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//初始化数据
initData();
recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
//设置布局
layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);
//添加分割线
recyclerView.addItemDecoration(new RecyclerViewDivider(this, LinearLayoutManager.VERTICAL));
adapter = new ContactAdapter(list);
recyclerView.setAdapter(adapter);
//item的点击事件
adapter.setOnItemClickListener(new ContactAdapter.OnItemClickListener() {
@Override
public void onItemClick(int position, Contact contact) {
Toast.makeText(MainActivity.this, contact.name + " - " + contact.pinYin, Toast.LENGTH_SHORT).show();
}
});
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
/**
* 查找(width>>1,1)点处的view,差不多是屏幕最上边,距顶部1px
* recyclerview上层的header所在的位置
*/
View itemView = recyclerView.findChildViewUnder(tvHeader.getMeasuredWidth() >> 1, 1);
/**
* recyclerview中如果有item占据了这个位置,那么header的text就为item的text
* 很显然,这个tiem是recyclerview的任意item
* 也就是说,recyclerview每滑过一个item,tvHeader就被赋了一次值
*/
if (itemView != null && itemView.getContentDescription() != null) {
tvHeader.setText(String.valueOf(itemView.getContentDescription()));
}
/**
* 指定可能印象外层header位置的item范围[-tvHeader.getMeasuredHeight()+1, tvHeader.getMeasuredHeight() + 1]
* 得到这个item
*/
View transInfoView = recyclerView.findChildViewUnder(
tvHeader.getMeasuredWidth() >> 1, tvHeader.getMeasuredHeight() + 1);
if (transInfoView != null && transInfoView.getTag() != null) {
int transViewStatus = (int) transInfoView.getTag();
int dealtY = transInfoView.getTop() - tvHeader.getMeasuredHeight();
if (transViewStatus == ContactAdapter.SHOW_HEADER_VIEW) {
/**
* 如果这个item有tag参数,而且是显示header的,正好是我们需要关注的item的header部分
*/
if (transInfoView.getTop() > 0) {
//说明item还在屏幕内,只是占据了外层header部分空间
tvHeader.setTranslationY(dealtY);
} else {
//说明item已经超出了recyclerview上边界,故此时外层的header的位置固定不变
tvHeader.setTranslationY(0);
}
} else if (transViewStatus == ContactAdapter.DISMISS_HEADER_VIEW) {
//如果此项的header隐藏了,即与外层的header无关,外层的header位置不变
tvHeader.setTranslationY(0);
}
}
}
});
//固定头item
tvHeader = (TextView) findViewById(R.id.tv_header);
if (list != null && list.size() > 0)
tvHeader.setText(list.get(0).firstPinYin);
//center tip view
tipView = (CenterTipView) findViewById(R.id.tv_center_tip);
//右侧字母表索引
rightContainer = (RightIndexView) findViewById(R.id.vg_right_container);
rightContainer.setData(firstList);
//右侧字母索引容器注册touch回调
rightContainer.setOnRightTouchMoveListener(this);
}
/**
* 右侧字母表touch回调
*
* @param position 当前touch的位置
* @param content 当前位置的内容
* @param isShow 显示与隐藏中间的tip view
*/
@Override
public void showTip(int position, final String content, boolean isShow) {
if (isShow) {
tipView.setVisibility(View.VISIBLE);
tipView.setText(content);
} else {
tipView.setVisibility(View.INVISIBLE);
}
for (int i = 0; i < list.size(); i++) {
if (list.get(i).firstPinYin.equals(content)) {
recyclerView.stopScroll();
int firstItem = layoutManager.findFirstVisibleItemPosition();
int lastItem = layoutManager.findLastVisibleItemPosition();
if (i <= firstItem) {
recyclerView.scrollToPosition(i);
} else if (i <= lastItem) {
int top = recyclerView.getChildAt(i - firstItem).getTop();
recyclerView.scrollBy(0, top);
} else {
recyclerView.scrollToPosition(i);
}
break;
}
}
}
/**
* 初始化数据。此部分略显复杂
* #数据不分开处理的话,会默认在list的开头。
* 微信的在最后!!!
*/
private void initData() {
if (list == null)
list = new ArrayList<>();
Contact contact = null;
//这儿使用的是静态数据
int size = Data.data.length;
//是否有非字母数据(拼音首字符不在26个字母范围当中)
boolean hasIncognizance = false;
//装载非字母数据的结合
ArrayList<Contact> incognizanceList = new ArrayList<>();
for (int i = 0; i < size; i++) {
contact = new Contact();
contact.name = Data.data[i];
contact.pinYin = PinYinUtil.toPinYin(contact.name);
contact.firstPinYin = PinYinUtil.firstPinYin(contact.pinYin);
if (!TextUtils.isEmpty(contact.firstPinYin)) {
char first = contact.firstPinYin.charAt(0);
//A(65), Z(90), a(97), z(122) 根据数据的类型分开装进集合
if (first < 'A' || (first > 'Z' && first < 'a') || first > 'z') {
//非字母
contact.firstPinYin = "#";
//标记含有#集合
hasIncognizance = true;
//添加数据到#集合
incognizanceList.add(contact);
} else {
//字母索引(set可以去重复)
set.add(contact.firstPinYin);
//添加数据到字母a-z集合
list.add(contact);
}
}
}
//对contact集合数据排序
Collections.sort(list);
//把排序后的字母顺序装进字母索引集合
Iterator<String> iterator = set.iterator();
while (iterator.hasNext()) {
firstList.add(iterator.next());
}
Collections.sort(firstList);
//最后加上#
if (hasIncognizance) {
//把#装进索引集合
firstList.add("#");
//把非字母的contact数据装进数据集合
list.addAll(incognizanceList);
}
//清空中间缓存集合
incognizanceList.clear();
set.clear();
}
}
感兴趣的朋友可以研究下,希望能有帮助