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


上周在某博客发现博主分享了一篇很经典的程序---------联系人效果。感觉很神秘很强大,但在阅读和理解博主的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




  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值