我尽量不打错别字,用词准确,不造成阅读障碍
高仿QQ侧滑效果,实现置顶、删除功能,完美适用于ListView和RecyclerView。
本侧滑很简单,只有右侧的侧滑,并没有其他酷炫的功能,希望给大家一个提示思路,如果需求简单的话可以自己照着写,不需要加入第三方库。本文是一步步完善功能的,最后会有完整代码。
原理
自定义ViewGroup,继承自FrameLayout,将“删除”、“置顶”两个按钮写到屏幕外面,然后通过监听手势滑动,调用ScroollTo()方法或ScrollBy()方法实现位移,实现侧滑效果,最后解决滑动冲突和其它Bug,完成删除、置顶功能。
代码
item的布局:
<com.teststudy.longl.myapplication3.MyRecyclerView2.SlideLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<RelativeLayout
android:id="@+id/ll_content_view"
android:layout_width="match_parent"
android:layout_height="70dp"
android:orientation="horizontal"
android:paddingEnd="10dp"
android:paddingStart="10dp"
android:visibility="visible">
<ImageView
android:id="@+id/iv_avatar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:src="@mipmap/ic_launcher" />
<TextView
android:id="@+id/tv_test2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:layout_toEndOf="@id/iv_avatar"
android:text="好友名称"
android:textColor="#000000"
android:textSize="18sp" />
<TextView
android:id="@+id/tv_test3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/tv_test2"
android:layout_gravity="center_vertical"
android:layout_marginStart="10dp"
android:layout_marginTop="10dp"
android:layout_toEndOf="@id/iv_avatar"
android:maxLines="1"
android:text="内容展示,随便写一些东西测试一下就好" />
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginStart="10dp"
android:layout_marginTop="15dp"
android:text="昨天" />
</RelativeLayout>
<LinearLayout
android:layout_width="200dp"
android:layout_height="70dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_toFirst"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@android:color/darker_gray"
android:gravity="center"
android:text="置顶"
android:textColor="@android:color/white"
android:textSize="22sp" />
<TextView
android:id="@+id/tv_delete"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@android:color/holo_red_light"
android:gravity="center"
android:text="删除"
android:textColor="@android:color/white"
android:textSize="22sp" />
</LinearLayout>
</com.teststudy.longl.myapplication3.MyRecyclerView2.SlideLayout>
“置顶”、“删除”正常是看不见的,只有侧滑时才看得见;SlideLayout就是我们要自定义的ViewGroup,继承FrameLayout。
SlideLayout代码:
public class SlideLayout extends FrameLayout {
private View mMenuView;
private int mMenuWidth;
private int mMenuHeight;
private int mContentWidth;
private Scroller mScroller;
private float startX;
private float downX;
private float downY;
private onSlideChangeListener mOnSlideChangeListener;
public SlideLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mMenuView = getChildAt(1);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mContentWidth = getMeasuredWidth();
mMenuWidth = mMenuView.getMeasuredWidth();
mMenuHeight = mMenuView.getMeasuredHeight();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
//将menu布局到右侧不可见
mMenuView.layout(mContentWidth, 0, mContentWidth + mMenuWidth, mMenuHeight);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = x;
break;
case MotionEvent.ACTION_MOVE:
final float dx = (int) (x - startX);
int disX = (int) (getScrollX() - dx);
if (disX <= 0) {
disX = 0;
}
scrollTo(Math.min(disX, mMenuWidth), getScrollY());
startX = x;
break;
case MotionEvent.ACTION_UP:
if (getScrollX() < mMenuWidth / 2) {
closeMenu();
} else {
openMenu();
}
break;
}
return true;
}
//拦截事件不传递给子view
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercept = false;
final float x = event.getX();
final float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = x;
downY = y;
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
break;
}
return intercept;
}
@Override
public void computeScroll() {
super.computeScroll();
//当动画执行完成以后,执行新的动画
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
public final void openMenu() {
mScroller.startScroll(getScrollX(), getScrollY(), mMenuWidth - getScrollX(), 0);
invalidate();
}
public final void closeMenu() {
mScroller.startScroll(getScrollX(), getScrollY(), 0 - getScrollX(), 0);
invalidate();
}
}
代码很简单,就不做过多介绍了,到这里会有许多Bug;
Bug1:滑动冲突
ListView的上下滑动和SlideLayout的左右滑动会有冲突,解决思路有两个:1.自定义ListView; 2.子View告知父View不要拦截。此处我们采用第二个方案。在OnTouchEvent() 方法中的ACTION_MOVE事件中做判断:
case MotionEvent.ACTION_MOVE:
final float dx = (int) (x - startX);
int disX = (int) (getScrollX() - dx);
if (disX <= 0) {
disX = 0;
}
scrollTo(Math.min(disX, mMenuWidth), getScrollY());
final float moveX = Math.abs(x - downX);
final float moveY = Math.abs(y - downY);
if (moveX > moveY && moveX > 10f) {
//父布局不要拦截子view的touch事件
getParent().requestDisallowInterceptTouchEvent(true);
}
startX = x;
break;
Bug2:点击事件
如果我此时在Adapter里面给整个item中ContentView部分(除删除、置顶以外的部分即左边部分)添加点击事件,侧滑的效果就不见了,这是因为父布局(SlideLayout)默认是不拦截子view的点击事件的,事件会由子View消费掉,不会返回给父View处理,所以我们需要拦截子View的点击事件,在onInterceptTouchEvent() 方法中的ACTION_MOVE情况中做处理:
case MotionEvent.ACTION_MOVE:
final float moveX = Math.abs(x - downX);
if (moveX > 10f) { //对touch事件进行拦截
intercept = true;
}
break;
这样会根据intercept来灵活判断是否拦截Touch事件。
Bug3:多个Item同时出现置顶、删除效果
QQ中只有一个Item会出现置顶、删除效果,不允许多个Item同时出现,任何操作都应该侧滑回位,如何实现?我们可以添加监听方法:
private onSlideChangeListener mOnSlideChangeListener;
public interface onSlideChangeListener {
void onMenuOpen(SlideLayout slideLayout);
void onMenuClose(SlideLayout slideLayout);
void onClick(SlideLayout slideLayout);
}
public void setOnSlideChangeListener(onSlideChangeListener onSlideChangeListener1) {
this.mOnSlideChangeListener = onSlideChangeListener1;
}
然后在onInterceptTouchEvent() 方法中ACTION_DOWN和openMenu()、closeMenu()中添加监听:
//... 省略部分
case MotionEvent.ACTION_DOWN:
downX = x;
downY = y;
if (mOnSlideChangeListener != null) {
mOnSlideChangeListener.onClick(this);
}
break;
//...省略部分
public final void openMenu() {
mScroller.startScroll(getScrollX(), getScrollY(), mMenuWidth - getScrollX(), 0);
invalidate();
if (mOnSlideChangeListener != null) {
mOnSlideChangeListener.onMenuOpen(this);
}
}
public final void closeMenu() {
mScroller.startScroll(getScrollX(), getScrollY(), 0 - getScrollX(), 0);
invalidate();
if (mOnSlideChangeListener != null) {
mOnSlideChangeListener.onMenuClose(this);
}
}
Adapter中可以设置监听了:
mSlideLayout.setOnSlideChangeListener(new SlideLayout.onSlideChangeListener() {
@Override
public void onMenuOpen(SlideLayout slideLayout) {
mSlideLayout = slideLayout;
}
@Override
public void onMenuClose(SlideLayout slideLayout) {
if (mSlideLayout != null) {
mSlideLayout = null;
}
}
@Override
public void onClick(SlideLayout slideLayout) {
if (mSlideLayout != null) {
mSlideLayout.closeMenu();
}
}
});
这样在openMenu时会赋值slideLayout,在任何click时会触发onInterceptTouchEvent() 的DOWN事件,进而执行closeMenu()方法。所以总的SliderLayout是这样的:
private View mMenuView;
private int mMenuWidth;
private int mMenuHeight;
private int mContentWidth;
private Scroller mScroller;
private float startX;
private float downX;
private float downY;
private onSlideChangeListener mOnSlideChangeListener;
public SlideLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mMenuView = getChildAt(1);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mContentWidth = getMeasuredWidth();
mMenuWidth = mMenuView.getMeasuredWidth();
mMenuHeight = mMenuView.getMeasuredHeight();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
//将menu布局到右侧不可见
mMenuView.layout(mContentWidth, 0, mContentWidth + mMenuWidth, mMenuHeight);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startX = x;
break;
case MotionEvent.ACTION_MOVE:
final float dx = (int) (x - startX);
int disX = (int) (getScrollX() - dx);
if (disX <= 0) {
disX = 0;
}
scrollTo(Math.min(disX, mMenuWidth), getScrollY());
final float moveX = Math.abs(x - downX);
final float moveY = Math.abs(y - downY);
if (moveX > moveY && moveX > 10f) {
//父布局不要拦截子view的touch事件
getParent().requestDisallowInterceptTouchEvent(true);
}
startX = x;
break;
case MotionEvent.ACTION_UP:
if (getScrollX() < mMenuWidth / 2) {
closeMenu();
} else {
openMenu();
}
break;
}
return true;
}
//拦截事件不传递给子view
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercept = false;
final float x = event.getX();
final float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
downX = x;
downY = y;
if (mOnSlideChangeListener != null) {
mOnSlideChangeListener.onClick(this);
}
break;
case MotionEvent.ACTION_MOVE:
final float moveX = Math.abs(x - downX);
if (moveX > 10f) { //对touch事件进行拦截
intercept = true;
}
break;
case MotionEvent.ACTION_UP:
break;
}
return intercept;
}
@Override
public void computeScroll() {
super.computeScroll();
//当动画执行完成以后,执行新的动画
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
public final void openMenu() {
mScroller.startScroll(getScrollX(), getScrollY(), mMenuWidth - getScrollX(), 0);
invalidate();
if (mOnSlideChangeListener != null) {
mOnSlideChangeListener.onMenuOpen(this);
}
}
public final void closeMenu() {
mScroller.startScroll(getScrollX(), getScrollY(), 0 - getScrollX(), 0);
invalidate();
if (mOnSlideChangeListener != null) {
mOnSlideChangeListener.onMenuClose(this);
}
}
public interface onSlideChangeListener {
void onMenuOpen(SlideLayout slideLayout);
void onMenuClose(SlideLayout slideLayout);
void onClick(SlideLayout slideLayout);
}
public void setOnSlideChangeListener(onSlideChangeListener onSlideChangeListener1) {
this.mOnSlideChangeListener = onSlideChangeListener1;
}
功能添加
两个功能:1.增加删除、置顶功能;2.增加悬浮框
4号被置顶了,悬浮框实现;在Adapter的getView()方法里添加如下代码:
mSlideLayout = (SlideLayout) view;
myViewHolder.ll_content_view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(mContext, "item被点击", Toast.LENGTH_SHORT).show();
}
});
myViewHolder.ll_content_view.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
int[] location = new int[2];
view.getLocationOnScreen(location);
View view1 = LayoutInflater.from(mContext).inflate(R.layout.item_background_popwindow, null);
PopupWindow popupWindow = new PopupWindow(view1, 300, 150);
popupWindow.setContentView(view1);
popupWindow.setOutsideTouchable(false);
popupWindow.setFocusable(true);
popupWindow.showAtLocation(view, Gravity.NO_GRAVITY, (view.getWidth() - popupWindow.getWidth()) / 2, location[1] - popupWindow.getHeight() - 5);
return false;
}
});
//置顶操作
myViewHolder.to_first.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//position在remove后会变,所以先把内容取出来
String content = mDataList.get(position);
mDataList.remove(position);
mDataList.add(0,content);
notifyDataSetInvalidated();
}
});
//删除操作
myViewHolder.delete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
mDataList.remove(position);
notifyDataSetInvalidated();
}
});
mSlideLayout.setOnSlideChangeListener(new SlideLayout.onSlideChangeListener() {
@Override
public void onMenuOpen(SlideLayout slideLayout) {
mSlideLayout = slideLayout;
}
@Override
public void onMenuClose(SlideLayout slideLayout) {
if(mSlideLayout != null) {
mSlideLayout = null;
}
}
@Override
public void onClick(SlideLayout slideLayout) {
if(mSlideLayout != null) {
mSlideLayout.closeMenu();
}
}
});
RecyclerView的Adapter写法
/**
* Created by longl on 2018/10/9.
*/
public class MyRecyclerView2Adapter extends RecyclerView.Adapter<MyRecyclerView2Adapter.MyViewHolder> {
private ArrayList<String> arrayList;
private Context mContext;
private SlideLayout mSlideLayout;
public MyRecyclerView2Adapter(Context context, ArrayList<String> dataList) {
this.arrayList = dataList;
this.mContext = context;
}
@Override
public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new MyViewHolder(LayoutInflater.from(mContext).inflate(R.layout.recycler_view_2_item, null));
}
@Override
public void onBindViewHolder(final MyViewHolder myViewHolder, int position) {
myViewHolder.textView.setText(arrayList.get(position));
myViewHolder.contentView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(mContext, "item被点击", Toast.LENGTH_SHORT).show();
}
});
myViewHolder.contentView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
int[] location = new int[2];
view.getLocationOnScreen(location);
View view1 = LayoutInflater.from(mContext).inflate(R.layout.item_background_popwindow, null);
PopupWindow popupWindow = new PopupWindow(view1, 300, 150);
popupWindow.setContentView(view1);
popupWindow.setOutsideTouchable(false);
popupWindow.setFocusable(true);
popupWindow.showAtLocation(view, Gravity.NO_GRAVITY, (view.getWidth() - popupWindow.getWidth()) / 2, location[1] - popupWindow.getHeight() - 5);
return false;
}
});
myViewHolder.to_first.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//position在remove后会变,所以先把内容取出来
String content = arrayList.get(myViewHolder.getAdapterPosition());
arrayList.remove(myViewHolder.getAdapterPosition());
arrayList.add(0, content);
notifyDataSetChanged();
}
});
myViewHolder.delete.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
arrayList.remove(myViewHolder.getAdapterPosition());
notifyDataSetChanged();
}
});
mSlideLayout = (SlideLayout) myViewHolder.itemView;
mSlideLayout.setOnSlideChangeListener(new SlideLayout.onSlideChangeListener() {
@Override
public void onMenuOpen(SlideLayout slideLayout) {
mSlideLayout = slideLayout;
}
@Override
public void onMenuClose(SlideLayout slideLayout) {
if (mSlideLayout != null) {
mSlideLayout = null;
}
}
@Override
public void onClick(SlideLayout slideLayout) {
if (mSlideLayout != null) {
mSlideLayout.closeMenu();
}
}
});
}
@Override
public int getItemCount() {
return arrayList.size();
}
public class MyViewHolder extends RecyclerView.ViewHolder {
private TextView textView;
private TextView to_first;
private TextView delete;
private RelativeLayout contentView;
public MyViewHolder(View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.tv_test2);
to_first = itemView.findViewById(R.id.tv_toFirst);
delete = itemView.findViewById(R.id.tv_delete);
contentView = itemView.findViewById(R.id.ll_content_view);
}
}
}
写法与ListView中的Adapter基本一致;
很简单吧!取消置顶如何做?我这里数据源是String,正常都是实体吧,可以在实体中添加变量标志是否已置顶,然后根据该标志在adapter中具体处理呀!这个方法看起来很low,但是简单高效,好处理啊,感觉也优化不到哪里去啊!
感谢以下文章:
https://blog.csdn.net/smile_Running/article/details/81916502
有问题欢迎讨论!
集合了一些简单自定义View的github地址:
https://github.com/longlong-2l/MySelfViewDemo
很简单,没有太多高深的用法,适合学习入门。