不论什么领域,在模仿一个东西的时候,我们首先要对它进行需求提取,这样才能保证做到”惟妙惟肖”。通过对QQ侧滑功能的分析,提取出了以下需求:
通过对需求的分析,首先会想到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的时候有以下几点需要注意:
加载上面的布局,然后设置给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()才能实现。接下来我们一步一步来实现其他的需求:
需求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是通过消息队列来实现同步的)同步操作。
有菜单展开的情况,那在某种操作下必然要恢复正常,即需求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;
}
}
现在可以尝试我们的滑动删除了,但是发现竟然支持可以多点触控,即可以侧滑多个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