基于Android RecyclerView實現(xiàn)宮格拖拽效果
前言
在Android發(fā)展的進程中,網(wǎng)格布局一直比較有熱度,其中一個原因是對用戶來說便捷操作,對app廠商而言也會帶來很多的曝光量,對于很多頭部app,展示網(wǎng)格菜單幾乎是必選項。實現(xiàn)網(wǎng)格的方式有很多種,比如GridView、GridLayout,TableLayout等,實際上,由于RecyclerView的靈活性和可擴展性很高,這些View基本沒必要去學(xué)了,為什么這樣說呢?主要原因是基于RecyclerView可以實現(xiàn)很多布局效果,傳統(tǒng)的很多Layout都可以通過RecyclerView去實現(xiàn),比如ViewPager、SlingTabLayout、DrawerLayout、ListView等,甚至連九宮格解鎖效果也可以實現(xiàn)。
當然,在很早之前,實現(xiàn)網(wǎng)格的拖拽效果主要是通過GridView去實現(xiàn)的,如果列數(shù)為1的話,那么GridView基本上就實現(xiàn)了ListView一樣的上下拖拽。
話說回來,我們現(xiàn)在基本不用去學(xué)習(xí)這類實現(xiàn)了,因為RecyclerView足夠強大,通過簡單的數(shù)據(jù)組裝,是完全可以替代GridView和ListView的。
效果
本篇我們會使用RecyclerView來實現(xiàn)網(wǎng)格拖拽,本篇將結(jié)合圖片分片案例,實現(xiàn)拖拽效果。

如果要實現(xiàn)網(wǎng)格菜單的拖拽,也是可以使用這種方式的,只要你的想象豐富,理論上,借助RecyclerView其實可以做出很多效果。

拖拽效果原理
拖動其實需要處理3個核心的問題,事件、圖像平移、數(shù)據(jù)交換。
事件處理
實際上無論傳統(tǒng)的拖拽效果還是最新的拖拽效果,都離不開事件處理,不過,好處就是,google為RecyclerView提供了ItemTouchHelper來處理這個問題,相比傳統(tǒng)的GridView實現(xiàn)方式,省去了很多事情,如動畫、目標查找等。
不過,我們回顧下原理,其實他們很多方面都是相似的,不同之處就是ItemTouchHelper 設(shè)計的非常好用,而且接口暴露的非常徹底,甚至能控制那些可以拖動、那些不能拖動、以及什么方向可以拖動,如果我們上、下、左、右四個方向都選中的話,斜對角拖動完全沒問題,
事件處理這里,GridView使用的方式相對傳統(tǒng),而ItemTouchHelper借助RecyclerView的一個接口(看樣子是開的后門),通過View自身去攔截事件.
public interface OnItemTouchListener {
//是否讓RecyclerView攔截事件
boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e);
//攔截之后處理RecyclerView的事件
void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e);
//監(jiān)聽禁止攔截事件的請求結(jié)果
void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept);
}
這種其實相對GridView來說簡單的多
圖像平移
無論是RecyclerView和傳統(tǒng)GridView拖動,都需要圖像平移。我們知道,RecyclerView和GridView本身是通過子View的邊界(left\top\right\bottom)來移動的,那么,在平移圖像的時候必然不能選擇這種方式,只能選擇Matrix 變化,也就是transitionX和transitionY的等。不同點是GridView的子View本身并不移動,而是將圖像繪制到一個GridView之外的View上,當然,實現(xiàn)上是比較復(fù)雜的。
但是,ItemTouchHelper設(shè)計比較巧妙的一點是,通過RecyclerView#ItemDecoration來實現(xiàn),在捕獲可以滑動的View之后,在繪制時對View進行偏移。
class ItemTouchUIUtilImpl implements ItemTouchUIUtil {
static final ItemTouchUIUtil INSTANCE = new ItemTouchUIUtilImpl();
@Override
public void onDraw(Canvas c, RecyclerView recyclerView, View view, float dX, float dY,
int actionState, boolean isCurrentlyActive) {
if (Build.VERSION.SDK_INT >= 21) {
if (isCurrentlyActive) {
Object originalElevation = view.getTag(R.id.item_touch_helper_previous_elevation);
if (originalElevation == null) {
originalElevation = ViewCompat.getElevation(view);
float newElevation = 1f + findMaxElevation(recyclerView, view);
ViewCompat.setElevation(view, newElevation);
view.setTag(R.id.item_touch_helper_previous_elevation, originalElevation);
}
}
}
view.setTranslationX(dX);
view.setTranslationY(dY);
}
//省略一些有關(guān)或者無關(guān)的代碼
}
不過,我們看到,Android 5.0的版本借助了setElevation 使得被拖拽View不被其他順序的View遮住,那Android 5.0之前是怎么實現(xiàn)的呢?
其實,做過TV app的都比較清楚,子View繪制順序可以通過下面方式調(diào)整,借助下面的方法,在TV上某個View獲取焦點之后,就不會被后面的View蓋住。
View#getChildDrawingOrder
ItemTouchHelper 同樣借助了此方法,為什么不統(tǒng)一一種呢,主要原因是getChildDrawingOrder是protected,總的來說,沒有通過setElevation方便。
private void addChildDrawingOrderCallback() {
if (Build.VERSION.SDK_INT >= 21) {
return; // we use elevation on Lollipop
}
if (mChildDrawingOrderCallback == null) {
mChildDrawingOrderCallback = new RecyclerView.ChildDrawingOrderCallback() {
@Override
public int onGetChildDrawingOrder(int childCount, int i) {
if (mOverdrawChild == null) {
return i;
}
int childPosition = mOverdrawChildPosition;
if (childPosition == -1) {
childPosition = mRecyclerView.indexOfChild(mOverdrawChild);
mOverdrawChildPosition = childPosition;
}
if (i == childCount - 1) {
return childPosition;
}
return i < childPosition ? i : i + 1;
}
};
}
mRecyclerView.setChildDrawingOrderCallback(mChildDrawingOrderCallback);
}
數(shù)據(jù)更新
數(shù)據(jù)更新這里其實ReyclerView的優(yōu)勢更加明顯,我們知道RecyclerView可以做到無requestLayout的局部刷新,性能更好。
@Override
public boolean onItemMove(int fromPosition, int toPosition) {
Collections.swap(mDataList, fromPosition, toPosition);
notifyItemMoved(fromPosition, toPosition);
return true;
}
不過,數(shù)據(jù)交換后還有一點需要處理,對Matrix相關(guān)屬性清理,防止無法落到指定區(qū)域。
@Override
public void clearView(View view) {
if (Build.VERSION.SDK_INT >= 21) {
final Object tag = view.getTag(R.id.item_touch_helper_previous_elevation);
if (tag instanceof Float) {
ViewCompat.setElevation(view, (Float) tag);
}
view.setTag(R.id.item_touch_helper_previous_elevation, null);
}
view.setTranslationX(0f);
view.setTranslationY(0f);
}
本篇實現(xiàn)
以上基本都是對ItemTouchHelper的原理梳理了,當然,如果你沒時間看上面的話,就看實現(xiàn)部分吧。
圖片分片
下面我們把多張圖片分割成 [行數(shù) x 列數(shù)]數(shù)量的圖片。
Bitmap srcInputBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.image_4);
Bitmap source = Bitmap.createScaledBitmap(srcInputBitmap, width, height, true);
srcInputBitmap.recycle();
int colCount = spanCount;
int rowCount = 6;
int spanImageWidthSize = source.getWidth() / colCount;
int spanImageHeightSize = (source.getHeight() - rowCount * padding/2) / rowCount;
Bitmap[] bitmaps = new Bitmap[rowCount * colCount];
for (int i = 0; i < rowCount; i++) {
for (int j = 0; j < colCount; j++) {
int y = i * spanImageHeightSize;
int x = j * spanImageWidthSize;
Bitmap bitmap = Bitmap.createBitmap(source, x, y, spanImageWidthSize, spanImageHeightSize);
bitmaps[i * colCount + j] = bitmap;
}
}
在這種過程我們一定要處理一個問題,如果我們對網(wǎng)格設(shè)置了邊界線(ItemDecoration)且是縱向布局的話,那么,縱向總高度要減去rowCount * bottomPadding,這里bottomPadding == padding/2,如下面代碼。
為什么要這么做呢?因為RecyclerView計算高度的時候,需要考慮這個高度,如果不去處理,那么ReyclerView可能不是禁止不動,而是會滑動,雖然影響不大,但是如果實現(xiàn)全屏效果,還能上下滑的話體驗比較差。
public class SimpleItemDecoration extends RecyclerView.ItemDecoration {
public int delta;
public SimpleItemDecoration(int padding) {
delta = padding;
}
@Override
public void getItemOffsets(Rect outRect, View view,
RecyclerView parent, RecyclerView.State state) {
int position = parent.getChildAdapterPosition(view);
RecyclerView.Adapter adapter = parent.getAdapter();
int viewType = adapter.getItemViewType(position);
if(viewType== Bean.TYPE_GROUP){
return;
}
GridLayoutManager layoutManager = (GridLayoutManager) parent.getLayoutManager();
//列數(shù)量
int cols = layoutManager.getSpanCount();
//position轉(zhuǎn)為在第幾列
int current = layoutManager.getSpanSizeLookup().getSpanIndex(position,cols);
//可有可無
int currentCol = current % cols;
int bottomPadding = delta / 2;
if (currentCol == 0) { //第0列左側(cè)貼邊
outRect.left = 0;
outRect.right = delta / 4;
outRect.bottom = bottomPadding;
} else if (currentCol == cols - 1) {
outRect.left = delta / 4;
outRect.right = 0;
outRect.bottom = bottomPadding;
//最后一列右側(cè)貼邊
} else {
outRect.left = delta / 4;
outRect.right = delta / 4;
outRect.bottom = bottomPadding;
}
}
}
更新數(shù)據(jù)
這部分是常規(guī)操作,主要目的是設(shè)置LayoutManager、Decoration、Adapter以及ItemTouchHelper,當然,ItemTouchHelper比較特殊,因為其內(nèi)部試下是ItemTouchHelper、OnItemTouchListener、Gesture的組合,因此封裝為attachToRecyclerView 來調(diào)用。
mLinearLayoutManager = new GridLayoutManager(this, spanCount, LinearLayoutManager.VERTICAL, false);
mLinearLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup(){
@Override
public int getSpanSize(int position) {
if(mAdapter.getItemViewType(position) == Bean.TYPE_GROUP){
return spanCount;
}
return 1;
}
});
mAdapter = new RecyclerViewAdapter();
mRecyclerView.setAdapter(mAdapter);
mRecyclerView.setLayoutManager(mLinearLayoutManager);
mRecyclerView.addItemDecoration(new SimpleItemDecoration(padding));
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new GridItemTouchCallback(mAdapter));
itemTouchHelper.attachToRecyclerView(mRecyclerView);
這里,我們主要還是關(guān)注ItemTouchHelper,在初始化的時候,我們給了一個GridItemTouchCallback,用于監(jiān)聽相關(guān)處理邏輯,最終通知Adapter調(diào)用notifyXXX更新View。
public class GridItemTouchCallback extends ItemTouchHelper.Callback {
private final ItemTouchCallback mItemTouchCallback;
public GridItemTouchCallback(ItemTouchCallback itemTouchCallback) {
mItemTouchCallback = itemTouchCallback;
}
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
// 上下左右拖動,但允許觸發(fā)刪除
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
return makeMovementFlags(dragFlags, 0);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
// 通知Adapter移動View
return mItemTouchCallback.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
// 通知Adapter刪除View
mItemTouchCallback.onItemRemove(viewHolder.getAdapterPosition());
}
@Override
public void onChildDraw(@NonNull Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
@Override
public void onChildDrawOver(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
Log.d("GridItemTouch","dx="+dX+", dy="+dY);
super.onChildDrawOver(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
}
}
這里,主要是對Flag的關(guān)注需要處理,第一參數(shù)是拖拽方向,第二個是刪除方向,我們本篇不刪除,因此,第二個參數(shù)為0即可。
public static int makeMovementFlags(int dragFlags, int swipeFlags) {
return makeFlag(ACTION_STATE_IDLE, swipeFlags | dragFlags)
| makeFlag(ACTION_STATE_SWIPE, swipeFlags)
| makeFlag(ACTION_STATE_DRAG, dragFlags);
}
總結(jié)
本篇到這里就結(jié)束了,我們利用RecyclerView實現(xiàn)了宮格圖片的拖拽效果,主要是借助ItemTouchHelper實現(xiàn),從ItemTouchHelper中我們能看到很多巧妙的的設(shè)計,里面有很多值得我們學(xué)習(xí)的技巧,特別是對事件的處理、繪制順序調(diào)整的方式,如果做吸頂,未嘗不是一種方案。
以上就是基于Android RecyclerView實現(xiàn)宮格拖拽效果的詳細內(nèi)容,更多關(guān)于Android RecyclerView宮格拖拽的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
淺談Android App開發(fā)中Fragment的創(chuàng)建與生命周期
這篇文章主要介紹了Android App開發(fā)中Fragment的創(chuàng)建與生命周期,文中詳細地介紹了Fragment的概念以及一些常用的生命周期控制方法,需要的朋友可以參考下2016-02-02
android dialog根據(jù)彈窗等級排序顯示的示例代碼
這篇文章主要介紹了android dialog根據(jù)彈窗等級排序顯示,本文通過示例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-10-10
Android開發(fā)學(xué)習(xí)之WallPaper設(shè)置壁紙詳細介紹與實例
這篇文章主要介紹了Android開發(fā)學(xué)習(xí)之WallPaper設(shè)置壁紙詳細介紹與實例,有需要的朋友可以參考一下2013-12-12
Android 一鍵清理、內(nèi)存清理功能實現(xiàn)
這篇文章主要介紹了Android 一鍵清理、內(nèi)存清理功能實現(xiàn),非常具有實用價值,需要的朋友可以參考下。2017-01-01
Android判斷軟鍵盤彈出并隱藏的簡單完美解決方法(推薦)
下面小編就為大家?guī)硪黄狝ndroid判斷軟鍵盤彈出并隱藏的簡單完美解決方法(推薦)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-10-10

