Android 仿微信联系人Demo(自定义View,Viewgroup)

来源:互联网 发布:ubuntu双系统启动顺序 编辑:程序博客网 时间:2024/06/05 08:56
 

Android 仿微信联系人Demo(自定义View,Viewgroup)

标签: android联系人自定义控件recyclerView
 2881人阅读 评论(7) 收藏 举报
 分类:


上周在某博客发现博主分享了一篇很经典的程序---------联系人效果。感觉很神秘很强大,但在阅读和理解博主的demo的同时也发现了一些冗余和不完美。于是带着宝宝的痛一咬牙自己开工了,大约花了一周的时间(当然我白天还得上班的),做出了这种效果。如下图:




now跟着我的思路分析开发过程。


一、界面的数据列表是recyclerview做的,或许listview也可以,但是没试过。

在xml中定义recyclerview,然后在activity中获取对象,创建适配器,设置数据给recyclerview。

数据是我自定义的静态数据

[html] view plain copy
  1. /**  
  2.  * @Author: duke  
  3.  * @DateTime: 2016-08-12 17:15  
  4.  * @Description:  
  5.  */  
  6. public class Data {  
  7.     //模拟数据  
  8.     public static final String[] data = {  
  9.             "安刚", "Android Studio",   
  10.             "杜科", "杜科>", "杜科》", "董卓", "达尔文", "董卓", "段誉",  
  11.             .........  
  12.             "周家大湾", "章鱼", "张三", "支那",  
  13.             "2哥", "4爷", "6+1", "0^_^0", "@126.com", "(!@#$%^&*)"};  
  14. }  

那么recyclerview的核心在于适配器:

[html] view plain copy
  1. /**  
  2.  * @Author: duke  
  3.  * @DateTime: 2016-08-12 15:34  
  4.  * @Description:  
  5.  */  
  6. public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ContactViewHolder> {  
  7.       
  8.   
  9.     @Override  
  10.     public void onBindViewHolder(final ContactViewHolder holder, final int position) {  
  11.         if (list == null || list.size() <= 0)  
  12.             return;  
  13.         final Contact contact = list.get(position);  
  14.         holder.tvHeader.setText(contact.firstPinYin);  
  15.         holder.tvName.setText(contact.name);  
  16.         holder.tvName.setOnClickListener(new View.OnClickListener() {  
  17.             @Override  
  18.             public void onClick(View v) {  
  19.                 onItemClickListener.onItemClick(holder.getLayoutPosition(), contact);  
  20.             }  
  21.         });  
  22.         if (position == 0) {  
  23.             holder.tvHeader.setText(contact.firstPinYin);  
  24.             holder.tvHeader.setVisibility(View.VISIBLE);  
  25.         } else {  
  26.             if (!TextUtils.equals(contact.firstPinYin, list.get(position - 1).firstPinYin)) {  
  27.                 holder.tvHeader.setVisibility(View.VISIBLE);  
  28.                 holder.tvHeader.setText(contact.firstPinYin);  
  29.                 holder.itemView.setTag(SHOW_HEADER_VIEW);  
  30.             } else {  
  31.                 holder.tvHeader.setVisibility(View.GONE);  
  32.                 holder.itemView.setTag(DISMISS_HEADER_VIEW);  
  33.             }  
  34.         }  
  35.         holder.itemView.setContentDescription(contact.firstPinYin);  
  36.     }  
  37.   
  38.     public interface OnItemClickListener {  
  39.         void onItemClick(int position, Contact contact);  
  40.     }  
  41. }  
item布局文件:

[html] view plain copy
  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     android:layout_width="match_parent"  
  3.     android:layout_height="wrap_content"  
  4.     android:orientation="vertical">  
  5.   
  6.     <TextView  
  7.     android:id="@+id/tv_header"  
  8.     android:layout_width="match_parent"  
  9.     android:layout_height="40dp"  
  10.     android:background="#FF9933"  
  11.     android:gravity="center_vertical"  
  12.     android:paddingLeft="10dp"  
  13.     android:text="A"  
  14.     android:textColor="@android:color/white"  
  15.     android:textSize="18sp" />  
  16.   
  17.     <TextView  
  18.         android:id="@+id/tv_name"  
  19.         android:layout_width="match_parent"  
  20.         android:layout_height="40dp"  
  21.         android:layout_centerVertical="true"  
  22.         android:background="@drawable/item_tv_name_selector"  
  23.         android:gravity="center_vertical"  
  24.         android:paddingLeft="10dp"  
  25.         android:text="name"  
  26.         android:textColor="@android:color/black"  
  27.         android:textSize="15sp" />  
  28. </LinearLayout>  

每个item都包含头部的字母栏和下面的名字栏。如果是第一个item当然需要显示header栏,然后后面的每个item设置数据的时候都需要判断当前所属首字母组和前面是否相同。不相同则说明是新的组,需要显示header;否则说明是相同的组,就隐藏header了。至此,即可实现带header栏的listview效果了。

所以Java bean至少需要2个属性,名称和首字母。

recyclerview默认没有带item之间的分割线,需要自己实现,还好我已经为你准备好了万能分割线工具类,文章地址:http://blog.csdn.net/fesdgasdgasdg/article/details/52003701

再来分析中间的提示view是字母弄的呢?我知道你们肯定会说:简单,弄个textview什么的,设置背景为一个圆角即可。

是,不过我这儿有点犯贱了,弄了自定义view,不要怕,后续文章我会根据我的理解发一系列的自定义view,自定义viewgroup文章,随时关注我。

代码:

1、属性文件:

[html] view plain copy
  1. <!-- properties for CenterTipView -->  
  2.     <declare-styleable name="CenterTipView">  
  3.         <attr name="bgColor" format="color|reference" />  
  4.         <attr name="textColor" format="color|reference" />  
  5.         <attr name="textSize" format="dimension|reference" />  
  6.         <attr name="text" format="string" />  
  7.         <attr name="type">  
  8.             <enum name="round" value="0" />  
  9.             <enum name="circle" value="1" />  
  10.         </attr>  
  11.     </declare-styleable>  

可以设置类型,即中间的view背景可以是圆形或者圆角矩形,可以设置背景、字体等信息。

2、类代码:

[html] view plain copy
  1. /**  
  2.  * @Author: duke  
  3.  * @DateTime: 2016-08-12 16:40  
  4.  * @Description: 中间提示view, 圆角矩形或者圆形背景  
  5.  */  
  6. public class CenterTipView extends View {  
  7.     //画笔  
  8.     private Paint mPaint;  
  9.     //画笔防锯齿  
  10.     private PaintFlagsDrawFilter paintFlagsDrawFilter;  
  11.     //图形背景颜色  
  12.     private int bgColor;  
  13.     //文本内容  
  14.     private String text;  
  15.     //文本颜色  
  16.     private int textColor;  
  17.     //字体大小  
  18.     private int textSize;  
  19.     //类型  
  20.     private int type;  
  21.     //圆角矩形或者圆形  
  22.     public static final int TYPE_ROUND = 0;  
  23.     public static final int TYPE_CIRCLE = 1;  
  24.   
  25.     private int mWidth;//宽  
  26.     private int mHeight;//高  
  27.     private int mMin;//宽高中的最小值  
  28.   
  29.     //文本边界  
  30.     private Rect mBound;  
  31.   
  32.     /**  
  33.      * 设置文本,重绘界面  
  34.      *  
  35.      * @param text  
  36.      */  
  37.     public void setText(String text) {  
  38.         this.text = text;  
  39.         postInvalidate();  
  40.     }  
  41.   
  42.     public String getText() {  
  43.         return text;  
  44.     }  
  45.   
  46.     public CenterTipView(Context context) {  
  47.         this(context, null);  
  48.     }  
  49.   
  50.     public CenterTipView(Context context, AttributeSet attrs) {  
  51.         this(context, attrs, 0);  
  52.     }  
  53.   
  54.     public CenterTipView(Context context, AttributeSet attrs, int defStyleAttr) {  
  55.         super(context, attrs, defStyleAttr);  
  56.         init(context, attrs);  
  57.     }  
  58.   
  59.     public void init(Context context, AttributeSet attrs) {  
  60.         mPaint = new Paint();  
  61.         //画笔防锯齿  
  62.         paintFlagsDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);  
  63.   
  64.         //获取自定义属性  
  65.         TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CenterTipView);  
  66.         int count = typedArray.getIndexCount();  
  67.         for (int i = 0; i < count; i++) {  
  68.             int attr = typedArray.getIndex(i);  
  69.             switch (attr) {  
  70.                 case R.styleable.CenterTipView_bgColor:  
  71.                     //背景颜色  
  72.                     bgColor = typedArray.getColor(attr, Color.BLACK);  
  73.                     break;  
  74.                 case R.styleable.CenterTipView_textColor:  
  75.                     //文本颜色  
  76.                     textColor = typedArray.getColor(attr, Color.WHITE);  
  77.                     break;  
  78.                 case R.styleable.CenterTipView_text:  
  79.                     //文本内容  
  80.                     text = typedArray.getString(attr);  
  81.                     break;  
  82.                 case R.styleable.CenterTipView_type:  
  83.                     //图形类型  
  84.                     type = typedArray.getInt(R.styleable.CenterTipView_type, 0);  
  85.                     break;  
  86.                 case R.styleable.CenterTipView_textSize:  
  87.                     //字体大小  
  88.                     textSize = typedArray.getDimensionPixelSize(attr,  
  89.                             (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));  
  90.                     break;  
  91.             }  
  92.         }  
  93.         //回收属性数组  
  94.         typedArray.recycle();  
  95.     }  
  96.   
  97.     @Override  
  98.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  99.         super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
  100.         mWidth = getMeasuredWidth();  
  101.         mHeight = getMeasuredHeight();  
  102.         mMin = Math.min(mWidth, mHeight);  
  103.         //依据最小的边,方便画圆  
  104.         setMeasuredDimension(mMin, mMin);  
  105.     }  
  106.   
  107.     @Override  
  108.     public void draw(Canvas canvas) {  
  109.         //防锯齿  
  110.         canvas.setDrawFilter(paintFlagsDrawFilter);  
  111.         mPaint.setColor(bgColor);  
  112.         if (type == TYPE_ROUND) {  
  113.             //画圆角矩形  
  114.             RectF rectF = new RectF(0, 0, mWidth, mHeight);  
  115.             canvas.drawRoundRect(rectF, 10, 10, mPaint);  
  116.         } else if (type == TYPE_CIRCLE) {  
  117.             //画圆  
  118.             canvas.drawCircle(mMin >> 1, mMin >> 1, mMin >> 1, mPaint);  
  119.         }  
  120.         //设置文本颜色  
  121.         mPaint.setColor(textColor);  
  122.         mPaint.setTextSize(textSize);  
  123.         if (mBound == null)  
  124.             mBound = new Rect();  
  125.         mPaint.getTextBounds(text, 0, text.length(), mBound);  
  126.         canvas.drawText(text, (mWidth - mBound.width()) >> 1, (mHeight + mBound.height()) >> 1, mPaint);  
  127.         super.draw(canvas);  
  128.     }  
  129. }  

代码并不多,也简单。首先继承view,重写必要的构造方法,在初始化方法中读取属性文件,获取相应的属性值。

在测量方法中做了简单处理,防止画圆时变形。

核心方法为onDraw。在里面来绘制界面需要显示的内容,根据xml设置的属性判断是画圆还是画圆角矩形。

然后绘制传递进来的字母索引文本。画文本时需要注意下面方法:

[html] view plain copy
  1. mBound = new Rect();  
  2. mPaint.getTextBounds(text, 0, text.length(), mBound);  
  3. canvas.drawText(text, (mWidth - mBound.width()) >> 1, (mHeight + mBound.height()) >> 1, mPaint);  

调用paint.getTextBounds方法,传递rect对象进去。然后没有返回值,字母rect就有值了呢?以前我一直过不去这儿。

其实涉及到值引用和地址引用问题。比喻下面代码:

[html] view plain copy
  1. public class Test {  
  2.     public static void main(String[] args) {  
  3.         int[] arr = {1,2,3};  
  4.         int a = 4;  
  5.         //修改值  
  6.         update(arr,a);  
  7.         //这儿打印值,有变化吗?  
  8.         System.out.println(arr[0]+"--"+a);  
  9.     }  
  10.       
  11.     public static void update(int[] tempArr,int tempA){  
  12.         tempArr[0] = 0;  
  13.         tempA = 0;  
  14.     }  
  15. }  

以前面试碰到这类问题,去试试。

继续说,paint.getTextBounds方法调用之后,文本的宽高范围数据已经保存到了rect对象中了。

然后根据width、height以及rect信息,来确定在canvas上怎么画text。有一点值得注意,默认画出的文本不是在当前view的(0,0)点。


工作完成了一半,剩下的就是右侧索引导航和上边的固定头怎么弄?先看右边的导航

其实也简单,发现有人用自定义view,纵向迭代绘制首字母集合即可,注意换行。在touch时根据按下处的高度计算出索引位置,确定文本是什么,然后重绘view。

然而,我有点逆火,正好不会自定义viewgroup,那就试试呗。

自定义viewgroup吧,去集成Linearlayout很简单的,设置好线性布局的方向为纵向,剩下的只负责添加child即可,不关心测量和布局了。

然后我又犯贱了一回,我继承的是viewgroup,也就意味着我需要自己去写操蛋的onMeasure和onLayout方法,以及自定义LayoutParams类等。

[html] view plain copy
  1. /**  
  2.  * @Author: duke  
  3.  * @DateTime: 2016-08-12 16:40  
  4.  * @Description: 右边索引导航view  
  5.  */  
  6. public class RightIndexView extends ViewGroup {  
  7.     private Context mContext;  
  8.     private ArrayList<String> list = new ArrayList<>();  
  9.   
  10.     //自定义属性(item的背景默认透明)  
  11.     private int rootBgColor;  
  12.     private int rootTouchBgColor;  
  13.     private int itemTouchBgColor;  
  14.     private int itemTextColor;  
  15.     private int itemTextTouchBgColor;  
  16.     private int itemTextSize;  
  17.   
  18.     public RightIndexView(Context context) {  
  19.         this(context, null);  
  20.     }  
  21.   
  22.     public RightIndexView(Context context, AttributeSet attrs) {  
  23.         this(context, attrs, 0);  
  24.     }  
  25.   
  26.     public RightIndexView(Context context, AttributeSet attrs, int defStyleAttr) {  
  27.         super(context, attrs, defStyleAttr);  
  28.         init(context, attrs);  
  29.     }  
  30.   
  31.     private void init(Context context, AttributeSet attrs) {  
  32.         //获取自定义属性  
  33.         TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RightIndexView);  
  34.         int count = typedArray.getIndexCount();  
  35.         for (int i = 0; i < count; i++) {  
  36.             int attr = typedArray.getIndex(i);  
  37.             switch (attr) {  
  38.                 case R.styleable.RightIndexView_rootBgColor:  
  39.                     //容器的背景颜色,没有则使用指定的默认值  
  40.                     rootBgColor = typedArray.getColor(attr, Color.parseColor("#80808080"));  
  41.                     break;  
  42.                 case R.styleable.RightIndexView_rootTouchBgColor:  
  43.                     //容器touch时的背景颜色  
  44.                     rootTouchBgColor = typedArray.getColor(attr, Color.parseColor("#EE808080"));  
  45.                     break;  
  46.                 case R.styleable.RightIndexView_itemTouchBgColor:  
  47.                     //item项的touch时背景颜色(item的背景默认透明)  
  48.                     itemTouchBgColor = typedArray.getColor(attr, Color.parseColor("#000000"));  
  49.                     break;  
  50.                 case R.styleable.RightIndexView_itemTextColor:  
  51.                     //item的文本颜色  
  52.                     itemTextColor = typedArray.getColor(attr, Color.parseColor("#FFFFFF"));  
  53.                     break;  
  54.                 case R.styleable.RightIndexView_itemTextTouchBgColor:  
  55.                     //item在touch时的文本颜色  
  56.                     itemTextTouchBgColor = typedArray.getColor(attr, Color.parseColor("#FF0000"));  
  57.                     break;  
  58.                 case R.styleable.RightIndexView_itemTextSize:  
  59.                     //item的文本字体(默认16)  
  60.                     itemTextSize = typedArray.getDimensionPixelSize(attr,  
  61.                             (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));  
  62.                     break;  
  63.             }  
  64.         }  
  65.         //回收属性数组  
  66.         typedArray.recycle();  
  67.         this.mContext = context;  
  68.         //设置容器默认背景  
  69.         setBackgroundColor(rootBgColor);  
  70.         //获取系统指定的最小move距离  
  71.         mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();  
  72.     }  
  73.   
  74.     /**  
  75.      * 测量子view和自己的大小  
  76.      * @param widthMeasureSpec  
  77.      * @param heightMeasureSpec  
  78.      */  
  79.     @Override  
  80.     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  81.         //获取系统测量的参数  
  82.         mWidth = MeasureSpec.getSize(widthMeasureSpec);  
  83.         mHeight = MeasureSpec.getSize(heightMeasureSpec);  
  84.         //第一个元素的top位置  
  85.         int top = 5;  
  86.         //item个数  
  87.         int size = 0;  
  88.         if (list != null && list.size() > 0) {  
  89.             //获取子孩子个数  
  90.             size = list.size();  
  91.             //上下各减去5px,除以个数计算出每个item的应有height  
  92.             mItemHeight = (mHeight - marginTop - marginBottom) / size;  
  93.         }  
  94.         /**  
  95.          * 此循环只是测量计算textview的上下左右位置数值,保存在其layoutParams中  
  96.          */  
  97.         for (int i = 0; i < size; i++) {  
  98.             TextView textView = (TextView) getChildAt(i);  
  99.             RightIndexView.LayoutParams layoutParams = (LayoutParams) textView.getLayoutParams();  
  100.             layoutParams.height = mItemHeight;//每个item指定应有的高度  
  101.             layoutParams.width = mWidth;//宽度为容器宽度  
  102.             layoutParams.top = top;//第一个item距上边5px  
  103.             top += mItemHeight;//往后每个item距上边+mItemHeight距离  
  104.         }  
  105.         /**  
  106.          * 由于此例特殊,宽度指定固定值,高度也是占满屏幕,不存在wrap_content情况  
  107.          * 故不需要根据子孩子的宽高来改动 mWidth 和 mHeight 的值。故最后直接保存初始计算的值  
  108.          */  
  109.         setMeasuredDimension(mWidth, mHeight);  
  110.     }  
  111.   
  112.     /**  
  113.      * 根据计算的子孩子值,在容器中布局排版子孩子  
  114.      */  
  115.     @Override  
  116.     protected void onLayout(boolean changed, int l, int t, int r, int b) {  
  117.         //获取子孩子个数  
  118.         int size = getChildCount();  
  119.         for (int i = 0; i < size; i++) {  
  120.             //得到特性顺序的子孩子  
  121.             TextView textView = (TextView) getChildAt(i);  
  122.             //拿到孩子中保存的数据  
  123.             RightIndexView.LayoutParams layoutParams = (LayoutParams) textView.getLayoutParams();  
  124.             //给子孩子布局位置(左,上,右,下)  
  125.             textView.layout(0, layoutParams.top, layoutParams.width, layoutParams.top + layoutParams.height);  
  126.         }  
  127.         /**  
  128.          * 结束了 onMeasure 和 onLayout 之后,当前容器的职责完成,onDraw 由子孩子自己画  
  129.          */  
  130.     }  
  131.   
  132.     public void setData(ArrayList<String> list) {  
  133.         if (list == null || list.size() <= 0)  
  134.             return;  
  135.         int size = list.size();  
  136.         this.list.addAll(list);  
  137.         for (int i = 0; i < size; i++) {  
  138.             addView(list.get(i), i);  
  139.         }  
  140.         //requestLayout();//重新measure和layout  
  141.         //invalidate();//重新draw  
  142.         //postInvalidate();//重新draw  
  143.     }  
  144.   
  145.     private void addView(String firstPinYin, int position) {  
  146.         TextView textView = new TextView(mContext);  
  147.         textView.setText(firstPinYin);  
  148.         textView.setBackgroundColor(Color.TRANSPARENT);  
  149.         textView.setTextColor(itemTextColor);  
  150.         textView.setTextSize(itemTextSize);  
  151.         textView.setGravity(Gravity.CENTER);  
  152.         textView.setTag(position);  
  153.         addView(textView, position);  
  154.     }  
  155.   
  156.       
  157.     /**  
  158.      * 必须重写的方法  
  159.      *  
  160.      * @return  
  161.      */  
  162.     @Override  
  163.     protected ViewGroup.LayoutParams generateDefaultLayoutParams() {  
  164.         return new RightIndexView.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);  
  165.     }  
  166.   
  167.     @Override  
  168.     protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {  
  169.         return new RightIndexView.LayoutParams(p);  
  170.     }  
  171.   
  172.     @Override  
  173.     public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {  
  174.         return new RightIndexView.LayoutParams(getContext(), attrs);  
  175.     }  
  176.   
  177.     @Override  
  178.     protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {  
  179.         return p instanceof RightIndexView.LayoutParams;  
  180.     }  
  181.   
  182.     public static class LayoutParams extends ViewGroup.LayoutParams {  
  183.         public int left;  
  184.         public int top;  
  185.   
  186.         public LayoutParams(Context c, AttributeSet attrs) {  
  187.             super(c, attrs);  
  188.         }  
  189.   
  190.         public LayoutParams(int width, int height) {  
  191.             super(width, height);  
  192.         }  
  193.   
  194.         public LayoutParams(ViewGroup.LayoutParams source) {  
  195.             super(source);  
  196.         }  
  197.     }  
  198. }  

1、上面代码中的onMeasure方法,是测量孩子们需要的总宽高,在告诉我自己该给多少宽高,这种情况是针对在xml中你只给我宽高都为wrap-content属性时。如果你给我了定值,比喻100dp,或者match_parent参数时,那onMeasure方法就几乎不要处理了。

2、onLayout方法就是根据测量好的宽高范围,来摆放这些孩子们。

上面2点也是viewgroup的核心代码,后续文章详解。

有了这些右边的效果几乎没啥问题了。

然而还有剩下的重点:

1、按下右边索引的某处时,被按下的child有背景色和文本颜色,真个右侧容器也有背景色。

2、在右边按下然后滑动时,滑到的child会跟着变色,滑过的会复原。

3、滑动到的位子,会在屏幕中间的view中体现出来。

4、滑动到的位置处的字母索引,会定位recyclerview的位置。

这些就得在右侧自定义viewgroup的ontouchevent方法中处理了。略需了解事件分发机制。

[html] view plain copy
  1. @Override  
  2.     public boolean onTouchEvent(MotionEvent event) {  
  3.         //当前手指位置的y坐标  
  4.         int y = (int) event.getY();  
  5.         //根据当前的y计算当前所在child的索引位置  
  6.         int tempIndex = computeViewIndexByY(y);  
  7.         if (tempIndex != -1) {  
  8.             //两头我留了点距离,不等于-1就代表没出界  
  9.             switch (event.getAction()) {  
  10.                 case MotionEvent.ACTION_DOWN:  
  11.                     yDown = y;  
  12.                     drawTextView(mOldViewIndex, false);  
  13.                     drawTextView(tempIndex, true);  
  14.                     mOldViewIndex = tempIndex;  
  15.                     if (onRightTouchMoveListener != null) {  
  16.                         onRightTouchMoveListener.showTip(tempIndex, ((TextView) getChildAt(tempIndex)).getText().toString(), true);  
  17.                     }  
  18.                     //设置root touch bg  
  19.                     setBackgroundColor(rootTouchBgColor);  
  20.                     break;  
  21.                 case MotionEvent.ACTION_MOVE:  
  22.                     yMove = y;  
  23.                     int distance = yDown - yMove;  
  24.                     if (Math.abs(distance) > mTouchSlop) {  
  25.                         //移动距离超出了一定范围  
  26.                         if (mOldViewIndex != tempIndex) {  
  27.                             //移动超出了当前元素  
  28.                             drawTextView(mOldViewIndex, false);  
  29.                             drawTextView(tempIndex, true);  
  30.                             mOldViewIndex = tempIndex;  
  31.                             setBackgroundColor(rootTouchBgColor);  
  32.                             if (onRightTouchMoveListener != null) {  
  33.                                 onRightTouchMoveListener.showTip(tempIndex, ((TextView) getChildAt(tempIndex)).getText().toString(), true);  
  34.                             }  
  35.                         }  
  36.                     }  
  37.                     break;  
  38.                 case MotionEvent.ACTION_UP:  
  39.                     drawTextView(mOldViewIndex, false);  
  40.                     drawTextView(tempIndex, false);  
  41.                     mOldViewIndex = tempIndex;  
  42.                     setBackgroundColor(rootBgColor);  
  43.                     if (onRightTouchMoveListener != null) {  
  44.                         onRightTouchMoveListener.showTip(tempIndex, ((TextView) getChildAt(tempIndex)).getText().toString(), false);  
  45.                     }  
  46.                     break;  
  47.             }  
  48.         } else {  
  49.             //出界了,可能是上边或者下边出界,恢复上边的元素  
  50.             if (list != null && list.size() > 0) {  
  51.                 drawTextView(mOldViewIndex, false);  
  52.                 setBackgroundColor(rootBgColor);  
  53.                 if (onRightTouchMoveListener != null) {  
  54.                     onRightTouchMoveListener.showTip(mOldViewIndex, ((TextView) getChildAt(mOldViewIndex)).getText().toString(), false);  
  55.                 }  
  56.             }  
  57.         }  
  58.         return true;  
  59.     }  

在自定义viewgroup中布局child时,你当然知道每个child的高度是多少,总高度减去两头的间距,再除以child个数即可。

那么在touch的时候你能拿到y值,在计算出child的索引。然后修改child的背景色等属性,拿到text再回调到main界面。

可以通过下面的代码,根据y值计算index:

[html] view plain copy
  1. /**  
  2.      * 依据y坐标、子孩子的高度和容器总高度计算当前textview的索引值  
  3.      */  
  4.     private int computeViewIndexByY(int y) {  
  5.         int returnValue;  
  6.         if (y < marginTop || y > (marginTop + mItemHeight * list.size())) {  
  7.             returnValue = -1;  
  8.         } else {  
  9.             int times = (y - marginTop) / mItemHeight;  
  10.             int remainder = (y - marginTop) % mItemHeight;  
  11.             if (remainder == 0) {  
  12.                 returnValue = --times;  
  13.             } else {  
  14.                 returnValue = times;  
  15.             }  
  16.         }  
  17.         return returnValue;  
  18.     }  

在activity这边设置回调

[html] view plain copy
  1. //右侧字母索引容器注册touch回调  
  2. rightContainer.setOnRightTouchMoveListener(this);  

在回调里面做你想做的吧,显示中间的view,回显带回来的text,定位recyclerview索引位置:

[html] view plain copy
  1. /**  
  2.      * 右侧字母表touch回调  
  3.      *  
  4.      * @param position 当前touch的位置  
  5.      * @param content  当前位置的内容  
  6.      * @param isShow   显示与隐藏中间的tip view  
  7.      */  
  8.     @Override  
  9.     public void showTip(int position, final String content, boolean isShow) {  
  10.         if (isShow) {  
  11.             tipView.setVisibility(View.VISIBLE);  
  12.             tipView.setText(content);  
  13.         } else {  
  14.             tipView.setVisibility(View.INVISIBLE);  
  15.         }  
  16.         for (int i = 0; i < list.size(); i++) {  
  17.             if (list.get(i).firstPinYin.equals(content)) {  
  18.                 recyclerView.stopScroll();  
  19.                 int firstItem = layoutManager.findFirstVisibleItemPosition();  
  20.                 int lastItem = layoutManager.findLastVisibleItemPosition();  
  21.                 if (i <= firstItem) {  
  22.                     recyclerView.scrollToPosition(i);  
  23.                 } else if (i <= lastItem) {  
  24.                     int top = recyclerView.getChildAt(i - firstItem).getTop();  
  25.                     recyclerView.scrollBy(0, top);  
  26.                 } else {  
  27.                     recyclerView.scrollToPosition(i);  
  28.                 }  
  29.                 break;  
  30.             }  
  31.         }  
  32.     }  

此处有一bug,我已修复。就是下面这个方法:

[html] view plain copy
  1. recyclerView.scrollToPosition(i);  
他的问题在于:如果当前的i位置已经出现在屏幕内了,但是不是在头部,再调用此方法时无效。除非你需要定位的position不在可见范围之内。我这用scrollBy处理了。

剩下最后一个问题就是固定头了。其实就是在recyclerview的上层放了一个header view:

main.xml部分代码:

[html] view plain copy
  1. <!-- 列表,占满屏幕 -->  
  2.     <android.support.v7.widget.RecyclerView  
  3.         android:id="@+id/recyclerView"  
  4.         android:layout_width="match_parent"  
  5.         android:layout_height="match_parent"  
  6.         android:overScrollMode="never"  
  7.         android:scrollbars="none" />  
  8.   
  9.     <!-- 固定头item,浮在recyclerview上层的顶部 -->  
  10.     <include layout="@layout/header" />  

header.xml代码:

[html] view plain copy
  1. <?xml version="1.0" encoding="utf-8"?>  
  2. <TextView xmlns:android="http://schemas.android.com/apk/res/android"  
  3.     android:id="@+id/tv_header"  
  4.     android:layout_width="match_parent"  
  5.     android:layout_height="40dp"  
  6.     android:background="#FF9933"  
  7.     android:gravity="center_vertical"  
  8.     android:paddingLeft="10dp"  
  9.     android:text="A"  
  10.     android:textColor="@android:color/white"  
  11.     android:textSize="18sp" />  

在滑动的时候根据item的header位置来让上层的header发生平移和赋值即可,当某item向上滑动时,header顶到上层的header,即让上层的header上移,顶多少就移动多少。

当item的header完全占据了上层的header位置时就让上层的header复位同时赋值。遇到下一个时重复上面的操作。没什么可神秘的。

依据分析得知需要在recyclerview的onscroll回调里面处理了:

[html] view plain copy
  1. recyclerView.addOnScrollListener(new OnScrollListener() {  
  2.             @Override  
  3.             public void onScrollStateChanged(RecyclerView recyclerView, int newState) {  
  4.                 super.onScrollStateChanged(recyclerView, newState);  
  5.             }  
  6.   
  7.             @Override  
  8.             public void onScrolled(RecyclerView recyclerView, int dx, int dy) {  
  9.                 /**  
  10.                  * 查找(width>>1,1)点处的view,差不多是屏幕最上边,距顶部1px  
  11.                  * recyclerview上层的header所在的位置  
  12.                  */  
  13.                 View itemView = recyclerView.findChildViewUnder(tvHeader.getMeasuredWidth() >> 1, 1);  
  14.   
  15.                 /**  
  16.                  * recyclerview中如果有item占据了这个位置,那么header的text就为item的text  
  17.                  * 很显然,这个tiem是recyclerview的任意item  
  18.                  * 也就是说,recyclerview每滑过一个item,tvHeader就被赋了一次值  
  19.                  */  
  20.                 if (itemView != null && itemView.getContentDescription() != null) {  
  21.                     tvHeader.setText(String.valueOf(itemView.getContentDescription()));  
  22.                 }  
  23.   
  24.                 /**  
  25.                  * 指定可能印象外层header位置的item范围[-tvHeader.getMeasuredHeight()+1, tvHeader.getMeasuredHeight() + 1]  
  26.                  * 得到这个item  
  27.                  */  
  28.                 View transInfoView = recyclerView.findChildViewUnder(  
  29.                         tvHeader.getMeasuredWidth() >> 1, tvHeader.getMeasuredHeight() + 1);  
  30.   
  31.                 if (transInfoView != null && transInfoView.getTag() != null) {  
  32.                     int transViewStatus = (int) transInfoView.getTag();  
  33.                     int dealtY = transInfoView.getTop() - tvHeader.getMeasuredHeight();  
  34.                     if (transViewStatus == ContactAdapter.SHOW_HEADER_VIEW) {  
  35.                         /**  
  36.                          * 如果这个item有tag参数,而且是显示header的,正好是我们需要关注的item的header部分  
  37.                          */  
  38.                         if (transInfoView.getTop() > 0) {  
  39.                             //说明item还在屏幕内,只是占据了外层header部分空间  
  40.                             tvHeader.setTranslationY(dealtY);  
  41.                         } else {  
  42.                             //说明item已经超出了recyclerview上边界,故此时外层的header的位置固定不变  
  43.                             tvHeader.setTranslationY(0);  
  44.                         }  
  45.                     } else if (transViewStatus == ContactAdapter.DISMISS_HEADER_VIEW) {  
  46.                         //如果此项的header隐藏了,即与外层的header无关,外层的header位置不变  
  47.                         tvHeader.setTranslationY(0);  
  48.                     }  
  49.                 }  
  50.             }  
  51.         });  

里面有一个方法需要了解,否则不好做:

[html] view plain copy
  1. /**  
  2.                  * 查找(width>>1,1)点处的view,差不多是屏幕最上边,距顶部1px  
  3.                  * recyclerview上层的header所在的位置  
  4.                  */  
  5.                 View itemView = recyclerView.findChildViewUnder(tvHeader.getMeasuredWidth() >> 1, 1);  

根据window中某一个指定的点,来查找recyclerview中的item,看谁在这个点上面。


到此分析完了,核心思想记这些。剩下的就靠你依据我的分析和源码去理解和尝试了。

demo地址:http://download.csdn.net/detail/fesdgasdgasdg/9607705