RecycleView的左滑實(shí)現(xiàn)
最終的效果圖是這樣的
要實(shí)現(xiàn)這樣的一個(gè)效果惭适,用到的關(guān)鍵技術(shù):
自定義view的基本知識(shí)+事件處理+其它知識(shí)
一.右邊的操作view
1.數(shù)據(jù)的組裝
我們可以把右邊的操作選項(xiàng)抽象出來數(shù)據(jù)對(duì)象即可,對(duì)于老司機(jī)的你們一看就懂。
public class SwipeMenuItem {
private static final int TITLE_SIZE = 20;//sp
private static final int WIDTH = 80;//dp
private int id;
private Context mContext;
private String title;
private Drawable icon;
private Drawable background;
private int titleColor;
private int titleSize;
private int width;
public SwipeMenuItem(Context context) {
mContext = context;
//設(shè)置默認(rèn)值
DisplayMetrics dm = mContext.getResources().getDisplayMetrics();
titleColor = Color.WHITE;
titleSize = TITLE_SIZE;
width = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, WIDTH, dm);
}
}
2.SwipeMenuView的簡(jiǎn)單擴(kuò)展(自定義view的一種吧)
public class SwipeMenuView extends LinearLayout implements View.OnClickListener {
private SwipeMenuLayout mLayout;
private SwipeMenu mMenu;
private OnMenuItemClickListener mOnMenuItemClickListener;
private int position;
public int getPosition() {
return position;
}
public void setPosition(int position) {
this.position = position;
}
public SwipeMenuView(SwipeMenu menu) {
super(menu.getContext());
setOrientation(LinearLayout.HORIZONTAL);
mMenu = menu;
List<SwipeMenuItem> items = mMenu.getMenuItems();
int id = 0;
for (SwipeMenuItem item : items) {
addItem(item, id++);
}
}
private void addItem(SwipeMenuItem item, int id) {
LayoutParams params = new LayoutParams(item.getWidth(),
LayoutParams.MATCH_PARENT);
LinearLayout parent = new LinearLayout(getContext());
parent.setId(id);
parent.setGravity(Gravity.CENTER);
parent.setOrientation(LinearLayout.VERTICAL);
parent.setLayoutParams(params);
parent.setBackgroundDrawable(item.getBackground());
parent.setOnClickListener(this);
addView(parent);
if (item.getIcon() != null) {
parent.addView(createIcon(item));
}
if (!TextUtils.isEmpty(item.getTitle())) {
parent.addView(createTitle(item));
}
}
private ImageView createIcon(SwipeMenuItem item) {
ImageView iv = new ImageView(getContext());
iv.setImageDrawable(item.getIcon());
return iv;
}
private TextView createTitle(SwipeMenuItem item) {
TextView tv = new TextView(getContext());
tv.setText(item.getTitle());
tv.setGravity(Gravity.CENTER);
tv.setTextSize(item.getTitleSize());
tv.setTextColor(item.getTitleColor());
return tv;
}
@Override
public void onClick(View v) {
if (mOnMenuItemClickListener != null && mLayout.isOpen()) {
mOnMenuItemClickListener.onMenuItemClick(position, mMenu, v.getId());
}
}
public interface OnMenuItemClickListener {
void onMenuItemClick(int position, SwipeMenu menu, int index);
}
public void setOnMenuItemClickListener(
OnMenuItemClickListener mOnMenuItemClickListener) {
this.mOnMenuItemClickListener = mOnMenuItemClickListener;
}
public void setLayout(SwipeMenuLayout mLayout) {
this.mLayout = mLayout;
}
}
說白了就是繼承LinearLayout 加了一個(gè)回調(diào)接口绣否,對(duì)于老司機(jī)的你們一看又懂了悲关。對(duì)于SwipeMenuLayout是什么怜校,我們后面會(huì)講的,別著急嗎颜屠?嘻嘻
二.RecyclerView.Adapter的處理
- 我們本著在不影響用戶原有的adapter的基礎(chǔ)上盡量不改或者少改。
對(duì)于RecyclerView的Adapter 我們都是繼承RecyclerView.Adapter鹰祸。<br /> - 主要是重寫onCreateViewHolder和onBindViewHolder方法甫窟。
- 對(duì)于onBindViewHolder方法完美不錯(cuò)任何處理,也沒有必要做蛙婴。<br />
- 主要是onCreateViewHolder方法粗井,這個(gè)方法返回是一條item的布局ui,對(duì)于我們這個(gè)效果在不改動(dòng)優(yōu)惠正常的view布局的情況下街图,我們可以這么做呢浇衬??餐济?耘擂?<br />
- 咦! 我們可以在原來的基礎(chǔ)上再套一層FrameLayout. 是的絮姆,沒錯(cuò)醉冤,老司機(jī)!篙悯!
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
//根據(jù)數(shù)據(jù)創(chuàng)建右邊的操作view
SwipeMenuView menuView = swipeMenuBuilder.create();
//包裝用戶的item布局
SwipeMenuLayout swipeMenuLayout = SwapWrapperUtils.wrap(parent, R.layout.item, menuView, new BounceInterpolator(), new LinearInterpolator());
MyViewHolder holder = new MyViewHolder(swipeMenuLayout);
setListener(parent, holder, viewType);
return holder;
}
SwapWrapperUtils.wrap 這個(gè)方法這里就不說了就是LayoutInflater加載布局蚁阳。
三.SwipeMenuLayout-view的設(shè)計(jì)
繼承自FrameLayout
講用戶的itemview這里我們叫Contentview,以及操作view我們叫MenuView鸽照,添加到這個(gè)FrameLayout上
setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.WRAP_CONTENT));
mMenuView.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT));
addView(mContentView);
addView(mMenuView);
設(shè)置初始狀態(tài)
我們要測(cè)量menuview的寬螺捐,高度就是Contentview的高。
我們要布局menuview移宅,在Contentview的右側(cè)归粉。
如圖:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//測(cè)量mMenuView的寬,高為mContentView的高
mMenuView.measure(MeasureSpec.makeMeasureSpec(0,
MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(
getMeasuredHeight(), MeasureSpec.EXACTLY));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mContentView.layout(0, 0, getMeasuredWidth(),
mContentView.getMeasuredHeight());
//在mContentView的右側(cè)
mMenuView.layout(getMeasuredWidth(), 0,
getMeasuredWidth() + mMenuView.getMeasuredWidth(),
mContentView.getMeasuredHeight());
}
控制滑動(dòng)
在android中根據(jù)滑動(dòng)來控制view有好多種,這里我們用layout方法
主要就是在recycleview滑動(dòng)時(shí)找到其中一條的位置position在ontouch方法中合適的時(shí)機(jī)將事件傳到該view上漏峰。什么時(shí)候觸發(fā)這個(gè)方法呢
糠悼,下文會(huì)說recycleview的處理事件。
我們寫一個(gè)方法將事件傳遞到此view上來控制menuView和contentView
public void onSwipe(MotionEvent event) {
mGestureDetector.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownX = (int) event.getX();
isFling = false;
break;
case MotionEvent.ACTION_MOVE:
//按下去-當(dāng)前的位置
int dis = (int) (mDownX - event.getX());
//menuView打開狀態(tài)dis+mMenuView寬
if (state == STATE_OPEN) {
dis += mMenuView.getWidth();
}
swipe(dis);
break;
case MotionEvent.ACTION_UP:
//快速滑動(dòng),或者超過了mMenuView寬的一半則打開,否則關(guān)閉
if (isFling || (mDownX - event.getX()) > (mMenuView.getWidth() / 2)) {
smoothOpenMenu();
} else {
smoothCloseMenu();
}
break;
}
}
/**
* 更改位置
* @param dis dis
*/
private void swipe(int dis) {
//mContentView的最大為mMenuView的寬
if (dis > mMenuView.getWidth()) {
dis = mMenuView.getWidth();
}
//mContentView-left的最小值為0即正常值
if (dis < 0) {
dis = 0;
}
//設(shè)置完mContentView的left就可以得出right以及mMenuView的left和right了
//主要是left,right
//left 最大值為-mMenuView.getWidth()
mContentView.layout(-dis, mContentView.getTop(),
mContentView.getWidth() - dis, getMeasuredHeight());
mMenuView.layout(mContentView.getWidth() - dis, mMenuView.getTop(),
mContentView.getWidth() + mMenuView.getWidth() - dis,
mMenuView.getBottom());
}
打開與關(guān)閉
借助computeScroll方法來不停的layout設(shè)置位置浅乔,代碼都對(duì)于位置的計(jì)算有注釋倔喂,生怕解釋不清楚铝条。
@Override
public void computeScroll() {
//讓mMenuView打開
if (state == STATE_OPEN) {
//是否停止了滑動(dòng)
if (mOpenScroller.computeScrollOffset()) {
swipe(mOpenScroller.getCurrX());
//重繪UI
postInvalidate();
}
} else {//讓mMenuView關(guān)閉
//mContentView的
if (mCloseScroller.computeScrollOffset()) {
//mBaseX為當(dāng)前的mContentView的left,可以結(jié)合
swipe(mBaseX - mCloseScroller.getCurrX());
postInvalidate();
}
}
}
/**
* 平滑的關(guān)閉mMenuView
*/
public void smoothCloseMenu() {
state = STATE_CLOSE;
mBaseX = -mContentView.getLeft();
//關(guān)閉是我們要讓mContentView的慢慢的減小,
//mCloseScroller.getCurrX()的范圍是(0,mBaseX)
mCloseScroller.startScroll(0, 0, mBaseX, 0, DURATION);
postInvalidate();
}
/**
* 平滑的打開mMenuView
*/
public void smoothOpenMenu() {
state = STATE_OPEN;
//其實(shí)我們這里是用到了Scroller類產(chǎn)生的值(當(dāng)然借助Interpolator來實(shí)現(xiàn)不同的值漸變,從而實(shí)現(xiàn)不同的效果)
//打開的時(shí)候mContentView的left從當(dāng)前的-mContentView.getLeft()到mMenuView.getWidth()
//在computeScroll方法中 swipe(mOpenScroller.getCurrX());即可
//mOpenScroller.getCurrX()的范圍是(-mContentView.getLeft(),mMenuView.getWidth())
//-mContentView.getLeft()為正值
mOpenScroller.startScroll(-mContentView.getLeft(), 0,
mMenuView.getWidth(), 0, DURATION);
postInvalidate();
}
四.RecyclerView的事件處理
首先我們要明白一點(diǎn)就是:我們要不影響用戶原來的item的點(diǎn)擊與長(zhǎng)按等事件。
我們肯定要重新事件的攔截與處理方法席噩。即onInterceptTouchEvent
與onTouchEvent方法班缰。我們需要在這2個(gè)方法里做如下的處理。
- 找到按下去的那一條
- 什么時(shí)候攔截各種down悼枢,move埠忘,up事件
- 處理各種down,move馒索,up事件
找到按下去的那一條
//找到當(dāng)前點(diǎn)擊坐標(biāo)下的所處于SwapRecyclerView的位置
int mFirstPosition = ((LinearLayoutManager) getLayoutManager()).findFirstVisibleItemPosition();
int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() == View.VISIBLE) {
child.getHitRect(mTouchFrame);
//判斷是否點(diǎn)擊到該控件上
if (mTouchFrame.contains(x, y)) {
mTouchPosition = mFirstPosition + i;
break;
}
}
}
找到了pos位置就可以 View view = getChildAt(mTouchPosition - mFirstPosition);
來獲取那個(gè)view了莹妒,就可以進(jìn)行事件的處理了。
child.getHitRect方法 绰上,我們看下sdkapi的注釋:
/**
找到控件占據(jù)的矩形區(qū)域的矩形坐標(biāo)
* Hit rectangle in parent's coordinates
*
返回的矩形 控件占據(jù)的矩形區(qū)域
* @param outRect The hit rectangle of the view.
*/
public void getHitRect(Rect outRect) {
if (hasIdentityMatrix() || mAttachInfo == null) {
outRect.set(mLeft, mTop, mRight, mBottom);
} else {
final RectF tmpRect = mAttachInfo.mTmpTransformRect;
tmpRect.set(0, 0, getWidth(), getHeight());
getMatrix().mapRect(tmpRect); // TODO: mRenderNode.mapRect(tmpRect)
outRect.set((int) tmpRect.left + mLeft, (int) tmpRect.top + mTop,
(int) tmpRect.right + mLeft, (int) tmpRect.bottom + mTop);
}
}
onInterceptTouchEvent 攔截 onTouch的處理 的搞基生活
down攔截的時(shí)候:
- menuView處于打開且點(diǎn)擊的不在menu區(qū)域
- 達(dá)到了滑動(dòng)的臨界值
- 這些情況都要交要我們處理旨怠,需要攔截(reutrn true),交給ontouch方法
//找到了
if (mTouchPosition != -1) {
//通過position得到item的viewHolder,并判斷合法性
View view = getChildAt(mTouchPosition - mFirstPosition);
RecyclerView.ViewHolder viewHolder = getChildViewHolder(view);
if (viewHolder.itemView instanceof SwipeMenuLayout) {
//menuView處于打開且點(diǎn)擊的不在menu區(qū)域
if (mTouchView != null && mTouchView.isOpen() && !inRangeOfView(mTouchView.getmMenuView(), event)) {
//攔截事件,交給自己的onTouch方法處理.
return true;
}
mTouchView = (SwipeMenuLayout) view;
} else {
throw new RuntimeException("viewHolder.itemView must be SwipeMenuLayout layout");
}
//將事件交給SwipeMenuLayout處理down事件
mTouchView.onSwipe(event);
}
//down事件,如果沒有打開menu,則不攔截,仍然交給系統(tǒng)
return handled;
然后在onTouchEven方法里處理down:
case MotionEvent.ACTION_DOWN:
//如果當(dāng)前是處于打開的且用戶按下去正好是打開menu的那行
if (mTouchPosition == oldPos && mTouchView != null
&& mTouchView.isOpen()) {
mTouchState = TOUCH_STATE_X;
mTouchView.onSwipe(event);
return true;
} else {
//如果不是直接關(guān)閉
if (mTouchView != null && mTouchView.isOpen()) {
mTouchView.smoothCloseMenu();
mTouchView = null;
return super.onTouchEvent(event);
}
}
break;
move攔截的時(shí)候:
達(dá)到滑動(dòng)的臨界值就可以攔截了return true了蜈块。
mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
case MotionEvent.ACTION_MOVE:
float dy = Math.abs((event.getY() - mDownY));
float dx = Math.abs((event.getX() - mDownX));
//達(dá)到了滑動(dòng)的臨界值
if (Math.abs(dy) > mTouchSlop || Math.abs(dx) > mTouchSlop) {
if (mTouchState == TOUCH_STATE_NONE) {
if (Math.abs(dy) > mTouchSlop) {//上下滑動(dòng)的
mTouchState = TOUCH_STATE_Y;
} else if (dx > mTouchSlop) {//左右滑動(dòng)的
mTouchState = TOUCH_STATE_X;
if (mOnSwipeListener != null) {
mOnSwipeListener.onSwipeStart(mTouchPosition);
}
}
}
return true;//攔截事件,交給自己的onTouch方法處理.
}
然后在onTouchEven方法里處理move:如果是左右我們才處理鉴腻,否則拜拜了您。
case MotionEvent.ACTION_MOVE:
//左右滑動(dòng)交給mTouchView處理,事件消費(fèi)了
if (mTouchState == TOUCH_STATE_X) {
if (mTouchView != null) {
mTouchView.onSwipe(event);
}
event.setAction(MotionEvent.ACTION_CANCEL);
super.onTouchEvent(event);
return true;
}
break;
最后up事件就簡(jiǎn)單了不需要攔截百揭,無非就是TOUCH_STATE_X狀態(tài)交給我們之前的SwipeMenuLayout處理打開還是關(guān)閉 爽哎, 以及 將一些變量的恢復(fù)為初始化狀態(tài)。
到此整個(gè)實(shí)現(xiàn)就完了信峻。
這里只分析一些核心的關(guān)鍵技術(shù)倦青,其它的都能看懂。
代碼下載地址: