HorizontalScrollView仿QQ侧滑删除

效果:

需求:

不论什么领域,在模仿一个东西的时候,我们首先要对它进行需求提取,这样才能保证做到”惟妙惟肖”。通过对QQ侧滑功能的分析,提取出了以下需求:

  1. 每个Item都可以侧滑,根据Item类型的不同,侧滑后显示的菜单项也不同(联系人/群组的菜单有:置顶,标为已读,删除, 通知类消息展示的菜单只有置顶和删除);
  2. 侧滑的过程中,如果滑动距离超过第一个菜单的宽度,抬起手指时会显示全部的菜单,即Item会滑动到最左端;
  3. 在向右滑动关闭菜单的过程中,如果滑动距离超过最后一个菜单的宽度,抬起手指时会关闭全部菜单, 即Item会恢复至正常展示状态;
  4. 如果Item的菜单呈展开状态,则点击此Item或按下其他Item,当前的Item的菜单将会关闭;
  5. 如果没有Item的菜单呈展开状态,点击Item时将进入聊天页面;
  6. 观察Item滑动的过程,发现其是匀速滑动, 而不是快速移动;
  7. 不能同时滑动多个Item;

通过对需求的分析,首先会想到HorizontalScrollView, 当然,重写Item的RooView的onTouchListener()也可以实现,但是普通的View只有scrollTo()和scrollBy()方法, 只能快速移动而不能匀速移动,导致滑动的过程很生硬。所以我们使用HorizontalScrollView来实现我们的效果。

* 布局:*

根布局其实没什么内容,就是一个ListView,这样就不贴代码了, 下面我们主要展示一下Item的布局内容:


<HorizontalScrollView  xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/horizontal_scrollview"
    android:layout_width="wrap_content"
    android:layout_height="70dp"
    android:fillViewport="true"
    android:scrollbars="none">
    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:orientation="horizontal"
        android:gravity="center">
        <LinearLayout
            android:id="@+id/content_layout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal"
            android:padding="12dp">
            <ImageView
                android:id="@+id/icon"
                android:layout_width="60dp"
                android:layout_height="60dp"
                android:src="@mipmap/ic_launcher"/>
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:layout_marginLeft="12dp"
                android:orientation="vertical">
                <TextView
                    android:id="@+id/name_text"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="Freeman"
                    android:textSize="15sp"
                    android:textColor="#333333"/>
                <TextView
                    android:id="@+id/content_text"
                    android:layout_width="wrap_content"
                    android:layout_height="match_parent"
                    android:gravity="bottom"
                    android:text="HorizontalScrollView实现QQ侧滑删除"
                    android:textSize="14sp"
                    android:textColor="#999999"/>
            LinearLayout>
            <TextView
                android:id="@+id/time_text"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:paddingRight="12dp"
                android:text="14:06"
                android:textSize="13sp"
                android:textColor="#999999"/>
        LinearLayout>
        <TextView
            android:layout_width="70dp"
            android:layout_height="match_parent"
            android:gravity="center"
            android:text="置顶"
            android:textSize="15sp"
            android:textColor="#FFFFFF"
            android:background="#999999"/>
        <TextView
            android:layout_width="85dp"
            android:layout_height="match_parent"
            android:gravity="center"
            android:text="标为已读"
            android:textSize="15sp"
            android:textColor="#FFFFFF"
            android:background="#FF9900"/>
        <TextView
            android:layout_width="70dp"
            android:layout_height="match_parent"
            android:gravity="center"
            android:text="删除"
            android:textSize="15sp"
            android:textColor="#FFFFFF"
            android:background="#FF0000"/>
    LinearLayout>
HorizontalScrollView>

在使用HorizontalScrollView的时候有以下几点需要注意:

  1. 它和ScrollView一样,只能有一个子布局,且一般为LinearLayout;
  2. 只有内容的宽度超过屏幕的宽度HorizontalScrollView才可以滑动;
  3. match_parent会失效。默认情况下HorizontalScrollView使用match_parent,如果内容没有占满屏幕宽度,则HorizontalScrollView的宽度不会达到屏幕的宽度,要想让HorizontalScrollView和屏幕宽度一样,需要添加 android:fillViewport=”true”属性。同样其子布局的match_parent也会失效,在这个布局中,id为content_layout的LinearLayout宽度为match_parent, 刚开始我认为它已经占满了整个屏幕的宽度,然后后面的菜单按键就会在屏幕之外,这样整个布局的跨度超过了屏幕宽度,HorizontalScrollView就可以自动滑动了,结果是我想多了;

实现:

step1. 让HorizontalScrollView滑动起来

加载上面的布局,然后设置给ListView设置了布局之后发现HorizontalView根本不能滑动,问题当然很明确,Item布局中match_paren失效,既然布局中不能设置,那我们只能动态设置了,通过以下代码设置Item中展示内容的布局宽度为屏幕宽度:

// 获取内容展示布局的布局参数
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) holder.contentLayout.getLayoutParams();
// 设置宽度为屏幕宽度
params.width = getResources().getDisplayMetrics().widthPixels;
// 重设布局参数
holder.contentLayout.setLayoutParams(params);

再次运行发现HorizontalScrollView可以滑动了,看起来效果还不错,但这只是一个开始。仔细分析一下上面其他的需求,我们发现即便使用HorizontalScrollView, 却依然需要重写onTouchListener()才能实现。接下来我们一步一步来实现其他的需求:

step2. 不要在路上逗留

需求2和需求3主要是对滑动距离的控制, 那么只要我们在手指弹起的时候对滑动距离进行判断,就可以很好地对其进行控制。代码实现如下:

    // 获取滑动的距离
    float distance = Math.abs(event.getX() - downX);
    if (distance > 0 && distance < dpToPx(70)) { // 如果滑动距离在70dp内(Button宽度)则回复至原来的位置
        v.post(new Runnable() {
            @Override
            public void run() {
                // 如果是向右滑动,则依然恢复至菜单展开状态
                if (isRight) {
                    ((HorizontalScrollView) view).fullScroll(View.FOCUS_RIGHT);
                } else {
                    ((HorizontalScrollView)view).fullScroll(View.FOCUS_LEFT);
                }
            }
        });
    } else { // 滑动距离超过70dp(Button宽度)则完全展开菜单或关闭菜单
        v.post(new Runnable() {
            @Override
            public void run() {
                if (isRight) {
                    ((HorizontalScrollView) view).fullScroll(View.FOCUS_LEFT);
                } else {
                    lastPosition = position;
                    ((HorizontalScrollView)view).fullScroll(View.FOCUS_RIGHT);
                }
            }
        });
    }

fullScroll()用来滑动至两端,View.FOCUS_LEFT表示滑动至最左端,即恢复至正常显示状态,View.FOCUS_RIGHT表示滑动至最右段,即展开菜单。但是直接使用fullScroll()是不起作用的,因为fullScroll()的操作是异步的,系统并不会等待fullScroll()执行完成,所以我们需要使用post将其添加到消息队列中(因为Android是通过消息队列来实现同步的)同步操作。

step3. 出来混,早晚要回去的

有菜单展开的情况,那在某种操作下必然要恢复正常,即需求4的描述。如果点击已展开的Item,则关闭此Item,如果按下其他的Item,则关闭已展开的Item,通过对需求的理解,我们可以得出需要处理点击事件和按下事件。具体代码如下:

// 处理按下其他Item则关闭已展开的Item的情况(lastPosition表示已展开的Item position)
if (lastPosition != -1 && lastPosition != position) {
    // 获取已展开的Item的RootView
    View openedItemView = getViewByPosition(listView, lastPosition);
    if (openedItemView != null) {
        final HorizontalScrollView horizontalScrollView =                       ((HorizontalScrollView)openedItemView.findViewById(R.id.horizontal_scrollview));
        // 将已展开的Item置位
        horizontalScrollView.smoothScrollTo(0, 0);
    }
}

需要注意的是:smoothScrollTo()是同步操作,直接使用就可以。这里需要说明一下获取ListView指定position的ItemView的实现:

private View getViewByPosition(ListView listView, int position) {
        // 获取当前可见的第一个Item的position
        int firstItemPos = listView.getFirstVisiblePosition();
        // 获取最后一个可见的Item的position
        int lastItemPos = firstItemPos + listView.getChildCount() - 1;
        if (position < firstItemPos || position > lastItemPos) {
            return listView.getAdapter().getView(position, null, listView);
        } else {
            int childIndex = position - firstItemPos;
            return listView.getChildAt(childIndex);
        }
    }

刚开始获取指定position的ItemView的时候使用了listView.getChildAt(childIndex), 结果在滑动到下一页的时候,点击Item就出现空指针的情况,看了下getChildAt()函数的源码,发现其返回的是只当前页可见的Item, 当然, getChildCount()返回也是当前页可见的Item的数量。

处理完了第一种情况,接下来我们处理点击已展开的Item的情况,这个问题其实很明显,我们只需要在UP事件中处理即可:

    // 获取滑动距离(区分滑动和点击)
    float distance = Math.abs(event.getX() - downX);
    if (distance == 0.0) {
        //点击已展开的Item的情况
        if (lastPosition == position) {
            v.post(new Runnable() {
                @Override
                public void run() {
                    ((HorizontalScrollView)view).fullScroll(View.FOCUS_LEFT);
                    lastPosition = -1;
                }
            });
        } else if (lastPosition == -1) {
            // 没有Item展开,点击时直接响应点击事件
           Toast.makeText(MainActivity.this, "触发了点击事件", Toast.LENGTH_SHORT).show();
        } else {
            // 对按下其他Item导致已展开的Item关闭的情况,对lastPosition进行置位
            lastPosition = -1;
        }
    }

step4. 有条不紊

现在可以尝试我们的滑动删除了,但是发现竟然支持可以多点触控,即可以侧滑多个Item。So easy! 只要我们关闭listView的多点触控即可解决此问题。在父布局的ListView中添加如下属性即可:

android:splitMotionEvents="false"

至此,我们通过HorizontalScrollView实现了QQ侧滑删除的全部需求(通过不同的Item加载不同的菜单很简单,根据类型对菜单项进行显示与隐藏即可)。

源码:

为了缩小文章的篇幅,在此我们只展示Adapter的getView的代码:

@Override
    public android.view.View getView(final int position, android.view.View convertView, final ViewGroup parent) {
        ViewHolder holder;
        if (convertView == null) {
            holder = new ViewHolder();
            convertView = getLayoutInflater().inflate(R.layout.item_layout, parent, false);
            holder.icon = (ImageView) convertView.findViewById(R.id.icon);
            holder.nameText = (TextView) convertView.findViewById(R.id.name_text);
            holder.contentText = (TextView) convertView.findViewById(R.id.content_text);
            holder.timeText = (TextView) convertView.findViewById(R.id.time_text);
            holder.contentLayout = (LinearLayout) convertView.findViewById(R.id.content_layout);
            holder.horizontalScrollView = (HorizontalScrollView) convertView.findViewById(R.id.horizontal_scrollview);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }

        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) holder.contentLayout.getLayoutParams();
        params.width = getResources().getDisplayMetrics().widthPixels;
        holder.contentLayout.setLayoutParams(params);
        holder.icon.setImageResource(data.get(position).icon);
        holder.nameText.setText(data.get(position).name);
        holder.contentText.setText(data.get(position).content);
        holder.timeText.setText(data.get(position).time);

        holder.horizontalScrollView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                final View view = v;
                switch (event.getAction()) {
                    case MotionEvent.ACTION_DOWN:
                        downX = event.getX();
                        if (lastPosition != -1 && lastPosition != position) {
                            View openedItemView = getViewByPosition(listView, lastPosition);
                            if (openedItemView != null) {
                                final HorizontalScrollView horizontalScrollView = ((HorizontalScrollView)openedItemView.findViewById(R.id.horizontal_scrollview));
                                horizontalScrollView.smoothScrollTo(0, 0);
                            }
                        }
                        break;
                    case MotionEvent.ACTION_MOVE:
                        if (event.getX() > lastXOffset) {
                            isRight = true;
                        } else {
                            isRight = false;
                        }
                        lastXOffset = event.getX();
                        break;
                    case MotionEvent.ACTION_UP:
                        float distance = Math.abs(event.getX() - downX);
                        if (distance == 0.0) {
                            if (lastPosition == position) {
                                v.post(new Runnable() {
                                    @Override
                                    public void run() {
                                        ((HorizontalScrollView)view).fullScroll(View.FOCUS_LEFT);
                                        lastPosition = -1;
                                    }
                                });
                            } else if (lastPosition == -1) {
                               Toast.makeText(MainActivity.this, "触发了点击事件", Toast.LENGTH_SHORT).show();
                            } else {
                                lastPosition = -1;
                            }
                        } else if (distance > 0 && distance < dpToPx(70)) {
                            v.post(new Runnable() {
                                @Override
                                public void run() {
                                    if (isRight) {
                                        ((HorizontalScrollView) view).fullScroll(View.FOCUS_RIGHT);
                                    } else {
                                        ((HorizontalScrollView)view).fullScroll(View.FOCUS_LEFT);
                                    }
                                }
                            });
                        } else {
                            v.post(new Runnable() {
                                @Override
                                public void run() {
                                    if (isRight) {
                                        ((HorizontalScrollView) view).fullScroll(View.FOCUS_LEFT);
                                    } else {
                                        lastPosition = position;
                                        ((HorizontalScrollView)view).fullScroll(View.FOCUS_RIGHT);
                                    }
                                }
                            });
                        }
                        break;
                    default:
                        break;
                }

                return false;
            }
        });

        return convertView;
    }
}

源码下载:

下载地址:HorizontalScrollView仿QQ侧滑删除Demo

你可能感兴趣的:(Android)