上周在某博客发现博主分享了一篇很经典的程序---------联系人效果。感觉很神秘很强大,但在阅读和理解博主的demo的同时也发现了一些冗余和不完美。于是带着宝宝的痛一咬牙自己开工了,大约花了一周的时间(当然我白天还得上班的),做出了这种效果。如下图:
now跟着我的思路分析开发过程。
一、界面的数据列表是recyclerview做的,或许listview也可以,但是没试过。
在xml中定义recyclerview,然后在activity中获取对象,创建适配器,设置数据给recyclerview。
数据是我自定义的静态数据
/** * @Author: duke * @DateTime: 2016-08-12 17:15 * @Description: */ public class Data { //模拟数据 public static final String[] data = { "安刚", "Android Studio", "杜科", "杜科>", "杜科》", "董卓", "达尔文", "董卓", "段誉", ......... "周家大湾", "章鱼", "张三", "支那", "2哥", "4爷", "6+1", "0^_^0", "@126.com", "(!@#$%^&*)"}; }
那么recyclerview的核心在于适配器:
/** * @Author: duke * @DateTime: 2016-08-12 15:34 * @Description: */ public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ContactViewHolder> { @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.tvName.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); } public interface OnItemClickListener { void onItemClick(int position, Contact contact); } }item布局文件:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> <TextView android:id="@+id/tv_header" android:layout_width="match_parent" android:layout_height="40dp" android:background="#FF9933" android:gravity="center_vertical" android:paddingLeft="10dp" android:text="A" android:textColor="@android:color/white" android:textSize="18sp" /> <TextView android:id="@+id/tv_name" android:layout_width="match_parent" android:layout_height="40dp" android:layout_centerVertical="true" android:background="@drawable/item_tv_name_selector" android:gravity="center_vertical" android:paddingLeft="10dp" android:text="name" android:textColor="@android:color/black" android:textSize="15sp" /> </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、属性文件:
<!-- properties for CenterTipView --> <declare-styleable name="CenterTipView"> <attr name="bgColor" format="color|reference" /> <attr name="textColor" format="color|reference" /> <attr name="textSize" format="dimension|reference" /> <attr name="text" format="string" /> <attr name="type"> <enum name="round" value="0" /> <enum name="circle" value="1" /> </attr> </declare-styleable>
可以设置类型,即中间的view背景可以是圆形或者圆角矩形,可以设置背景、字体等信息。
2、类代码:
/** * @Author: duke * @DateTime: 2016-08-12 16:40 * @Description: 中间提示view, 圆角矩形或者圆形背景 */ public class CenterTipView extends View { //画笔 private Paint mPaint; //画笔防锯齿 private PaintFlagsDrawFilter paintFlagsDrawFilter; //图形背景颜色 private int bgColor; //文本内容 private String text; //文本颜色 private int textColor; //字体大小 private int textSize; //类型 private int type; //圆角矩形或者圆形 public static final int TYPE_ROUND = 0; public static final int TYPE_CIRCLE = 1; private int mWidth;//宽 private int mHeight;//高 private int mMin;//宽高中的最小值 //文本边界 private Rect mBound; /** * 设置文本,重绘界面 * * @param text */ public void setText(String text) { this.text = text; postInvalidate(); } public String getText() { return text; } public CenterTipView(Context context) { this(context, null); } public CenterTipView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CenterTipView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs); } public void init(Context context, AttributeSet attrs) { mPaint = new Paint(); //画笔防锯齿 paintFlagsDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); //获取自定义属性 TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CenterTipView); int count = typedArray.getIndexCount(); for (int i = 0; i < count; i++) { int attr = typedArray.getIndex(i); switch (attr) { case R.styleable.CenterTipView_bgColor: //背景颜色 bgColor = typedArray.getColor(attr, Color.BLACK); break; case R.styleable.CenterTipView_textColor: //文本颜色 textColor = typedArray.getColor(attr, Color.WHITE); break; case R.styleable.CenterTipView_text: //文本内容 text = typedArray.getString(attr); break; case R.styleable.CenterTipView_type: //图形类型 type = typedArray.getInt(R.styleable.CenterTipView_type, 0); break; case R.styleable.CenterTipView_textSize: //字体大小 textSize = typedArray.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics())); break; } } //回收属性数组 typedArray.recycle(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); mWidth = getMeasuredWidth(); mHeight = getMeasuredHeight(); mMin = Math.min(mWidth, mHeight); //依据最小的边,方便画圆 setMeasuredDimension(mMin, mMin); } @Override public void draw(Canvas canvas) { //防锯齿 canvas.setDrawFilter(paintFlagsDrawFilter); mPaint.setColor(bgColor); if (type == TYPE_ROUND) { //画圆角矩形 RectF rectF = new RectF(0, 0, mWidth, mHeight); canvas.drawRoundRect(rectF, 10, 10, mPaint); } else if (type == TYPE_CIRCLE) { //画圆 canvas.drawCircle(mMin >> 1, mMin >> 1, mMin >> 1, mPaint); } //设置文本颜色 mPaint.setColor(textColor); mPaint.setTextSize(textSize); if (mBound == null) mBound = new Rect(); mPaint.getTextBounds(text, 0, text.length(), mBound); canvas.drawText(text, (mWidth - mBound.width()) >> 1, (mHeight + mBound.height()) >> 1, mPaint); super.draw(canvas); } }
代码并不多,也简单。首先继承view,重写必要的构造方法,在初始化方法中读取属性文件,获取相应的属性值。
在测量方法中做了简单处理,防止画圆时变形。
核心方法为onDraw。在里面来绘制界面需要显示的内容,根据xml设置的属性判断是画圆还是画圆角矩形。
然后绘制传递进来的字母索引文本。画文本时需要注意下面方法:
mBound = new Rect(); mPaint.getTextBounds(text, 0, text.length(), mBound); canvas.drawText(text, (mWidth - mBound.width()) >> 1, (mHeight + mBound.height()) >> 1, mPaint);
调用paint.getTextBounds方法,传递rect对象进去。然后没有返回值,字母rect就有值了呢?以前我一直过不去这儿。
其实涉及到值引用和地址引用问题。比喻下面代码:
public class Test { public static void main(String[] args) { int[] arr = {1,2,3}; int a = 4; //修改值 update(arr,a); //这儿打印值,有变化吗? System.out.println(arr[0]+"--"+a); } public static void update(int[] tempArr,int tempA){ tempArr[0] = 0; tempA = 0; } }
以前面试碰到这类问题,去试试。
继续说,paint.getTextBounds方法调用之后,文本的宽高范围数据已经保存到了rect对象中了。
然后根据width、height以及rect信息,来确定在canvas上怎么画text。有一点值得注意,默认画出的文本不是在当前view的(0,0)点。
工作完成了一半,剩下的就是右侧索引导航和上边的固定头怎么弄?先看右边的导航
其实也简单,发现有人用自定义view,纵向迭代绘制首字母集合即可,注意换行。在touch时根据按下处的高度计算出索引位置,确定文本是什么,然后重绘view。
然而,我有点逆火,正好不会自定义viewgroup,那就试试呗。
自定义viewgroup吧,去集成Linearlayout很简单的,设置好线性布局的方向为纵向,剩下的只负责添加child即可,不关心测量和布局了。
然后我又犯贱了一回,我继承的是viewgroup,也就意味着我需要自己去写操蛋的onMeasure和onLayout方法,以及自定义LayoutParams类等。
/** * @Author: duke * @DateTime: 2016-08-12 16:40 * @Description: 右边索引导航view */ 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; 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); RightIndexView.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); //拿到孩子中保存的数据 RightIndexView.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); } /** * 必须重写的方法 * * @return */ @Override protected ViewGroup.LayoutParams generateDefaultLayoutParams() { return new RightIndexView.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); } @Override protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { return new RightIndexView.LayoutParams(p); } @Override public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { return new RightIndexView.LayoutParams(getContext(), attrs); } @Override protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { return p instanceof RightIndexView.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); } } }
1、上面代码中的onMeasure方法,是测量孩子们需要的总宽高,在告诉我自己该给多少宽高,这种情况是针对在xml中你只给我宽高都为wrap-content属性时。如果你给我了定值,比喻100dp,或者match_parent参数时,那onMeasure方法就几乎不要处理了。
2、onLayout方法就是根据测量好的宽高范围,来摆放这些孩子们。
上面2点也是viewgroup的核心代码,后续文章详解。
有了这些右边的效果几乎没啥问题了。
然而还有剩下的重点:
1、按下右边索引的某处时,被按下的child有背景色和文本颜色,真个右侧容器也有背景色。
2、在右边按下然后滑动时,滑到的child会跟着变色,滑过的会复原。
3、滑动到的位子,会在屏幕中间的view中体现出来。
4、滑动到的位置处的字母索引,会定位recyclerview的位置。
这些就得在右侧自定义viewgroup的ontouchevent方法中处理了。略需了解事件分发机制。
@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; }
在自定义viewgroup中布局child时,你当然知道每个child的高度是多少,总高度减去两头的间距,再除以child个数即可。
那么在touch的时候你能拿到y值,在计算出child的索引。然后修改child的背景色等属性,拿到text再回调到main界面。
可以通过下面的代码,根据y值计算index:
/** * 依据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; }
在activity这边设置回调
//右侧字母索引容器注册touch回调 rightContainer.setOnRightTouchMoveListener(this);
在回调里面做你想做的吧,显示中间的view,回显带回来的text,定位recyclerview索引位置:
/** * 右侧字母表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; } } }
此处有一bug,我已修复。就是下面这个方法:
recyclerView.scrollToPosition(i);他的问题在于:如果当前的i位置已经出现在屏幕内了,但是不是在头部,再调用此方法时无效。除非你需要定位的position不在可见范围之内。我这用scrollBy处理了。
剩下最后一个问题就是固定头了。其实就是在recyclerview的上层放了一个header view:
main.xml部分代码:
<!-- 列表,占满屏幕 --> <android.support.v7.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" />
header.xml代码:
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/tv_header" android:layout_width="match_parent" android:layout_height="40dp" android:background="#FF9933" android:gravity="center_vertical" android:paddingLeft="10dp" android:text="A" android:textColor="@android:color/white" android:textSize="18sp" />
在滑动的时候根据item的header位置来让上层的header发生平移和赋值即可,当某item向上滑动时,header顶到上层的header,即让上层的header上移,顶多少就移动多少。
当item的header完全占据了上层的header位置时就让上层的header复位同时赋值。遇到下一个时重复上面的操作。没什么可神秘的。
依据分析得知需要在recyclerview的onscroll回调里面处理了:
recyclerView.addOnScrollListener(new 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); } } } });
里面有一个方法需要了解,否则不好做:
/** * 查找(width>>1,1)点处的view,差不多是屏幕最上边,距顶部1px * recyclerview上层的header所在的位置 */ View itemView = recyclerView.findChildViewUnder(tvHeader.getMeasuredWidth() >> 1, 1);
根据window中某一个指定的点,来查找recyclerview中的item,看谁在这个点上面。
到此分析完了,核心思想记这些。剩下的就靠你依据我的分析和源码去理解和尝试了。
demo地址:http://download.csdn.net/detail/fesdgasdgasdg/9607705