寫在前面:參考YoKey,感謝颤陶。
附上他的鏈接:http://www.reibang.com/p/d30fd8da4eac
主頁
界面我們分成4個viewType,分別是我的頻道頭瘪板,我的頻道內(nèi)容肉迫,其他頻道頭逸吵,其他頻道內(nèi)容宛乃。
// 我的頻道 標題部分
public static final int TYPE_MY_CHANNEL_HEADER = 0;
// 我的頻道
public static final int TYPE_MY = 1;
// 其他頻道 標題部分
public static final int TYPE_OTHER_CHANNEL_HEADER = 2;
// 其他頻道
public static final int TYPE_OTHER = 3;
- 填充假數(shù)據(jù)
//我的頻道的數(shù)據(jù)
final List<ChannelData> myItems = new ArrayList<>();
for (int i = 0 ; i < 20 ; i++){
ChannelData channelData = new ChannelData();
channelData.setName("頻道"+i);
myItems.add(channelData);
}
//其他頻道的數(shù)據(jù)
final List<ChannelData> otherItems = new ArrayList<>();
for (int i = 0 ; i < 20 ; i++){
ChannelData channelData = new ChannelData();
channelData.setName("其他"+i);
otherItems.add(channelData);
}
- 首頁代碼
開啟網(wǎng)格模式
//網(wǎng)格模式 4列
GridLayoutManager gridLayoutManager = new GridLayoutManager(this,4);
mRecy.setLayoutManager(gridLayoutManager);
使用ItemTouchHelper的大概模型,這里itemTouchHelper就是實現(xiàn)item拖拽和滑動的關(guān)鍵類磷账,new對象的時候里面要放itemTouchHelper.CallBak回調(diào)峭沦。最后通過attachToRecyclerView方法綁定RecyclerView。
ItemDragHelperCallback callback = new ItemDragHelperCallback();
final ItemTouchHelper helper = new ItemTouchHelper(callback);//實現(xiàn)item的拖拽和滑動
helper.attachToRecyclerView(mRecy);//通過attachToRecyclerView方法綁定RecyclerView
new adapter對象
//adapter對象
final ChannelAdapter adapter = new ChannelAdapter(this,helper,myItems,otherItems);
我們看看里面的四個參數(shù)逃糟,先看看ChannelAdapter的構(gòu)造方法吼鱼。
public ChannelAdapter(Context context, ItemTouchHelper mItemTouchHelper, List<ChannelData> mMyChannelItems, List<ChannelData> mOtherChannelItems) {
this.mInflater = LayoutInflater.from(context);
this.mItemTouchHelper = mItemTouchHelper;
this.mMyChannelItems = mMyChannelItems;
this.mOtherChannelItems = mOtherChannelItems;
}
第一個參數(shù)上下文環(huán)境,第二個參數(shù)就是ItemTouchHelper對象绰咽,第三個參數(shù)是我的頻道item列表菇肃,第四個參數(shù)是其他頻道item列表。
因為4種類型都寫在一個界面里了取募,但是頭要單獨占一行琐谤,非頭的item要幾個(具體數(shù)自己定)才占一行,這時通過網(wǎng)格布局管理器中setSpanSizeLookup方法進行操作了玩敏,這個方法會根據(jù)position來設(shè)置span size 斗忌,這個我們的span count是4 如果是頭我們就給span size 為4 ,內(nèi)容就給1旺聚。具體分析看代碼
//根據(jù)position來設(shè)置span size 這個我們的span count是4 如果是頭我們就給span size 為4 內(nèi)容就給1
gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
int viewType = adapter.getItemViewType(position);
//三元運算 如果是內(nèi)容就給1 是頭我們就給span size 為4
return viewType == ChannelAdapter.TYPE_MY || viewType == ChannelAdapter.TYPE_OTHER ? 1 : 4;
}
});
mRecy.setAdapter(adapter);
我們看到里面有一個getItemViewType方法织阳,點進去看下
@Override
public int getItemViewType(int position) {
//不同position對應的viewType
if (position == 0) {
return TYPE_MY_CHANNEL_HEADER;
} else if (position == mMyChannelItems.size() + 1) {
return TYPE_OTHER_CHANNEL_HEADER;
} else if (position > 0 && position < mMyChannelItems.size() + 1) {
return TYPE_MY;
} else {
return TYPE_OTHER;
}
}
哦,原來是一個復寫的方法砰粹,看看RecyclerView里對這個方法的描述
/**
* Return the view type of the item at <code>position</code> for the purposes
* of view recycling.
*
* <p>The default implementation of this method returns 0, making the assumption of
* a single view type for the adapter. Unlike ListView adapters, types need not
* be contiguous. Consider using id resources to uniquely identify item view types.
*
* @param position position to query
* @return integer value identifying the type of the view needed to represent the item at
* <code>position</code>. Type codes need not be contiguous.
*/
public int getItemViewType(int position) {
return 0;
}
原來是根據(jù)position的位置獲取這個item的viewtype唧躲,那繼續(xù)看我們復寫的那個方法,position==0是那就是我的頻道頭碱璃,我的頻道的item數(shù)量+1就是其他頻道頭弄痹,在這之間就是我的頻道,剩下的就是其他頻道了厘贼,這樣類型就是出來界酒,在往上看,三元運算符嘴秸,意思很明顯,就是當viewType是我的頻道或者其他頻道的時候返回值就為1,否則就為4岳掐,返回值就是span size凭疮。
ItemTouchHelper.Callback類
這里我們寫了一個ItemTouchHelper.Callback的繼承類〈觯看看這類里面一些必須的方法执解。
設(shè)置滑動類型
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
int dragFlags;
RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
if (manager instanceof GridLayoutManager || manager instanceof StaggeredGridLayoutManager){
dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT ;
}else {
dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
}
// 如果想支持滑動(刪除)操作, swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END
int swipeFlags = 0 ;
//返回一個整數(shù)類型的標識,用于判斷Item那種移動行為是允許的
return makeMovementFlags(dragFlags,swipeFlags);
}
如上代碼纲酗,如果屬于網(wǎng)格類型衰腌,就可以上下左右的拖拽,不然就只能上下拖拽觅赊,滑動的話是禁止的右蕊。返回一個整數(shù)類型的標識,用于判斷Item那種移動行為是允許的吮螺。
拖拽切換Item的回調(diào)
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
if (viewHolder.getItemViewType() != target.getItemViewType()){
return false;
}
//instanceof 這個對象是否是這個特定類或者是它的子類的一個實例饶囚。
if (recyclerView.getAdapter() instanceof OnItemMoveListener){
OnItemMoveListener listener = (OnItemMoveListener) recyclerView.getAdapter();
listener.onItemMove(viewHolder.getAdapterPosition(),target.getAdapterPosition());
}
return true;
}
如果Item切換了位置,返回true鸠补;反之萝风,返回false。onMove()是在拖動到新位置時候的回調(diào)方法紫岩,我們在這里做數(shù)組集合的交換操作规惰,在這里我們把它暴露出去,交給Adapter自己處理泉蝌。注意這里一個判斷recyclerView.getAdapter() instanceof OnItemMoveListener歇万,在寫adapter的時候會實現(xiàn)OnItemMoveListener這個接口。
public interface OnItemMoveListener {
void onItemMove(int fromPosition,int toPosition);
}
這個類里還復寫了item被選中和調(diào)用完畢后item的狀態(tài)方法
/**
* item被選中時改變item的背景
*/
@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
//不在閑置的狀態(tài)
if (actionState != ItemTouchHelper.ACTION_STATE_IDLE){
if (viewHolder instanceof OnDragVHListener){
OnDragVHListener itemViewHolder = (OnDragVHListener) viewHolder;
itemViewHolder.onItemSelected();
}
}
super.onSelectedChanged(viewHolder, actionState);
}
/**
* 用戶操作完畢或者動畫完畢后調(diào)用梨与,恢復item的背景和透明度
*/
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
if (viewHolder instanceof OnDragVHListener){
OnDragVHListener itemViewHolder = (OnDragVHListener) viewHolder;
itemViewHolder.onItemFinished();
}
super.clearView(recyclerView, viewHolder);
}
這里注意下OnDragVHListener
public interface OnDragVHListener {
/**
* Item被選中時觸發(fā)
*/
void onItemSelected();
/**
* Item在拖拽結(jié)束/滑動結(jié)束后觸發(fā)
*/
void onItemFinished();
}
后續(xù)中堕花,因為拖拽操作只被設(shè)定在我的頻道里操作,所以目前只有我的頻道ViewHolder實現(xiàn)了OnDragVHListener粥鞋,這里就改變了一下背景色缘挽。
/**
* 我的頻道
*/
class MyViewHolder extends RecyclerView.ViewHolder implements OnDragVHListener {
private TextView textView;
private ImageView imgEdit;
public MyViewHolder(View itemView) {
super(itemView);
textView = (TextView) itemView.findViewById(R.id.tv);
imgEdit = (ImageView) itemView.findViewById(R.id.img_edit);
}
/**
* item 被選中時
*/
@Override
public void onItemSelected() {
textView.setBackgroundResource(R.drawable.bg_channel_p);
}
/**
* item 取消選中時
*/
@Override
public void onItemFinished() {
textView.setBackgroundResource(R.drawable.bg_channel);
}
}
長按拖拽,這里其他頻道不需要拖拽呻粹,所以返回false,對于我的頻道則手動調(diào)用ItemTouchHelper的startDrag方法啟動拖拽壕曼。
/**
* isLongPressDragEnabled()如果返回true,則支持長按拖拽等浊,
* 這里“其他頻道”等不需要拖拽腮郊,所以返回false,手動調(diào)用ItemTouchHelper的startDrag方法啟動拖拽筹燕。
*/
@Override
public boolean isLongPressDragEnabled() {
return false;
}
/**
* 不支持滑動
* @return false
*/
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
ChannelAdapter
對于這個類轧飞,吧代碼貼上看吧衅鹿,注釋已經(jīng)寫的很詳細了。
public RecyclerView.ViewHolder onCreateViewHolder(final ViewGroup parent, int viewType) {
switch (viewType) {
case TYPE_MY_CHANNEL_HEADER:
case TYPE_MY:
case TYPE_OTHER_CHANNEL_HEADER:
case TYPE_OTHER:
}
return null;
}
拆開看吧
我的頻道頭,就是進入和取消編輯狀態(tài)
case TYPE_MY_CHANNEL_HEADER:
view = mInflater.inflate(R.layout.item_my_channel_header, parent, false);
final MyChannelHeaderViewHolder holder = new MyChannelHeaderViewHolder(view);
holder.tvBtnEdit.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (!isEditMode) {
startEditMode((RecyclerView) parent);
holder.tvBtnEdit.setText(R.string.finish);
} else {
cancelEditMode((RecyclerView) parent);
holder.tvBtnEdit.setText(R.string.edit);
}
}
});
return holder;
我的頻道过咬,這里有三種監(jiān)聽大渤,一種是點擊,一種是長按掸绞,還一種是觸摸
case TYPE_MY:
view = mInflater.inflate(R.layout.item_my, parent, false);
final MyViewHolder myViewHolder = new MyViewHolder(view);
//點擊
myViewHolder.textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = myViewHolder.getAdapterPosition();
if (isEditMode) {
RecyclerView recyclerView = (RecyclerView) parent;
//目標view也就是即將要去其他頻道的那個item
// position是 mMyChannelItems.size() + COUNT_PRE_OTHER_HEADER 也既是我的頻道頭+其他頻道頭+我的頻道內(nèi)容全部item
View targetView = recyclerView.getLayoutManager().findViewByPosition(mMyChannelItems.size() + COUNT_PRE_OTHER_HEADER);
View currentView = recyclerView.getLayoutManager().findViewByPosition(position);
// 如果targetView不在屏幕內(nèi),則indexOfChild為-1 此時不需要添加動畫,因為此時notifyItemMoved自帶一個向目標移動的動畫
// 如果在屏幕內(nèi),則添加一個位移動畫
if (recyclerView.indexOfChild(targetView) >= 0) {
int targetX, targetY;
//獲取到網(wǎng)格布局的列數(shù)
RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
int spanCount = ((GridLayoutManager) manager).getSpanCount();
if ((mMyChannelItems.size() - 1) % spanCount == 0) {
//這種情況 移動后高度發(fā)生變化 (少了一行)
View preTargetView = recyclerView.getLayoutManager().findViewByPosition(mMyChannelItems.size() + COUNT_PRE_OTHER_HEADER - 1);
targetX = preTargetView.getLeft();
targetY = preTargetView.getTop();
} else {
targetX = targetView.getLeft();
targetY = targetView.getTop();
}
moveMyToOther(myViewHolder);
startAnimation(recyclerView, currentView, targetX, targetY);
} else {
//targetView不在屏幕內(nèi),則indexOfChild為-1 此時不需要添加動畫
moveMyToOther(myViewHolder);
}
} else {
//不是編輯狀態(tài)下點擊我的頻道item
mChannelItemClickListener.OnItemClick(v, position - 1);
}
}
});
//長按
myViewHolder.textView.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
if (!isEditMode) {
RecyclerView recyclerView = ((RecyclerView) parent);
startEditMode(recyclerView);
//header 按鈕文字 改成"完成"
View view = recyclerView.getChildAt(0);
if (view == recyclerView.getLayoutManager().findViewByPosition(0)) {
TextView tvBtnEdit = (TextView) view.findViewById(R.id.tv_btn_edit);
tvBtnEdit.setText(R.string.finish);
}
}
mItemTouchHelper.startDrag(myViewHolder);
return true;
}
});
//touch
myViewHolder.textView.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (isEditMode) {
switch (MotionEventCompat.getActionMasked(event)) {
case MotionEvent.ACTION_DOWN:
startTime = System.currentTimeMillis();
break;
case MotionEvent.ACTION_MOVE:
if (System.currentTimeMillis() - startTime > SPACE_TIME) {
mItemTouchHelper.startDrag(myViewHolder);
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
startTime = 0;
break;
}
}
return false;
}
});
return myViewHolder;
這里我想說下判斷targetView在不在屏幕里的那個方法indexOfChild泵三,以及item移動后高度發(fā)生變化的情況。
先看indexOfChild
/**
* Returns the position in the group of the specified child view.
*
* @param child the view for which to get the position
* @return a positive integer representing the position of the view in the
* group, or -1 if the view does not exist in the group
*/
public int indexOfChild(View child) {
final int count = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < count; i++) {
if (children[i] == child) {
return i;
}
}
return -1;
}
看他里面實現(xiàn)是把所有的指view都輪詢一遍衔掸,看看我們傳入的view在不在輪詢的里面烫幕,不在就return -1。在看高度變化敞映,想想在什么時候高度會發(fā)生變化较曼,當我的頻道多出一個item的時候,移動后我們這行少了驱显。告訴發(fā)生變化诗芜,其他頻道的位置也跟著變化,所有添加一個(mMyChannelItems.size() - 1) % spanCount == 0的判斷埃疫。
其他頻道
case TYPE_OTHER:
view = mInflater.inflate(R.layout.item_other, parent, false);
final OtherViewHolder otherHolder = new OtherViewHolder(view);
otherHolder.textView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
RecyclerView recyclerView = ((RecyclerView) parent);
RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
int currentPiosition = otherHolder.getAdapterPosition();
// 如果RecyclerView滑動到底部,移動的目標位置的y軸 - height
View currentView = manager.findViewByPosition(currentPiosition);
// 目標位置的前一個item 即當前MyChannel的最后一個
View preTargetView = manager.findViewByPosition(mMyChannelItems.size() - 1 + COUNT_PRE_MY_HEADER);
// 如果targetView不在屏幕內(nèi),則為-1 此時不需要添加動畫,因為此時notifyItemMoved自帶一個向目標移動的動畫
// 如果在屏幕內(nèi),則添加一個位移動畫
if (recyclerView.indexOfChild(preTargetView) >= 0) {
int targetX = preTargetView.getLeft();
int targetY = preTargetView.getTop();
int targetPosition = mMyChannelItems.size() - 1 + COUNT_PRE_OTHER_HEADER;
GridLayoutManager gridLayoutManager = ((GridLayoutManager) manager);
int spanCount = gridLayoutManager.getSpanCount();
// target 在最后一行第一個
if ((targetPosition - COUNT_PRE_MY_HEADER) % spanCount == 0) {
View targetView = manager.findViewByPosition(targetPosition);
targetX = targetView.getLeft();
targetY = targetView.getTop();
} else {
targetX += preTargetView.getWidth();
// 最后一個item可見
if (gridLayoutManager.findLastVisibleItemPosition() == getItemCount() - 1) {
// 最后的item在最后一行第一個位置
if ((getItemCount() - 1 - mMyChannelItems.size() - COUNT_PRE_OTHER_HEADER) % spanCount == 0) {
// RecyclerView實際高度 > 屏幕高度 && RecyclerView實際高度 < 屏幕高度 + item.height
int firstVisiblePostion = gridLayoutManager.findFirstVisibleItemPosition();
if (firstVisiblePostion == 0) {
// FirstCompletelyVisibleItemPosition == 0 即 內(nèi)容不滿一屏幕 , targetY值不需要變化
// // FirstCompletelyVisibleItemPosition != 0 即 內(nèi)容滿一屏幕 并且 可滑動 , targetY值 + firstItem.getTop
if (gridLayoutManager.findFirstCompletelyVisibleItemPosition() != 0) {
int offset = (-recyclerView.getChildAt(0).getTop()) - recyclerView.getPaddingTop();
targetY += offset;
}
} else { // 在這種情況下 并且 RecyclerView高度變化時(即可見第一個item的 position != 0),
// 移動后, targetY值 + 一個item的高度
targetY += preTargetView.getHeight();
}
}
} else {
System.out.println("呵呵噠");
}
}
// 如果當前位置是otherChannel可見的最后一個
// 并且 當前位置不在grid的第一個位置
// 并且 目標位置不在grid的第一個位置
// 則 需要延遲250秒 notifyItemMove , 這是因為這種情況 , 并不觸發(fā)ItemAnimator , 會直接刷新界面
// 導致我們的位移動畫剛開始,就已經(jīng)notify完畢,引起不同步問題
if (currentPiosition == gridLayoutManager.findLastVisibleItemPosition()
&& (currentPiosition - mMyChannelItems.size() - COUNT_PRE_OTHER_HEADER) % spanCount != 0
&& (targetPosition - COUNT_PRE_MY_HEADER) % spanCount != 0) {
moveOtherToMyWithDelay(otherHolder);
} else {
moveOtherToMy(otherHolder);
}
startAnimation(recyclerView, currentView, targetX, targetY);
} else {
moveOtherToMy(otherHolder);
}
}
});
return otherHolder;
動畫
rivate void startAnimation(RecyclerView recyclerView, final View currentView, int targetX, int targetY) {
final ViewGroup viewGroup = (ViewGroup) recyclerView.getParent();
final ImageView mirrorView = addMirrorView(viewGroup, recyclerView, currentView);
Animation animation = getTranslateAnimator(
targetX - currentView.getLeft(), targetY - currentView.getTop());
currentView.setVisibility(View.INVISIBLE);
mirrorView.startAnimation(animation);
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
viewGroup.removeView(mirrorView);
if (currentView.getVisibility() == View.INVISIBLE) {
currentView.setVisibility(View.VISIBLE);
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
}
添加鏡像,鏡像ImageView啟動位移動畫的同時伏恐,調(diào)用notifyItemMove。
/**
* 添加需要移動的 鏡像View
*/
private ImageView addMirrorView(ViewGroup parent, RecyclerView recyclerView, View view) {
view.destroyDrawingCache();
view.setDrawingCacheEnabled(true);
final ImageView mirrorView = new ImageView(recyclerView.getContext());
Bitmap bitmap = Bitmap.createBitmap(view.getDrawingCache());
mirrorView.setImageBitmap(bitmap);
view.setDrawingCacheEnabled(false);
int[] locations = new int[2];
view.getLocationOnScreen(locations);
int[] parenLocations = new int[2];
recyclerView.getLocationOnScreen(parenLocations);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(bitmap.getWidth(), bitmap.getHeight());
params.setMargins(locations[0], locations[1] - parenLocations[1], 0, 0);
parent.addView(mirrorView, params);
return mirrorView;
}