Andorid分組Item頂部懸停 + 交互同步
需求概述
項目中某些頁面中的分組數(shù)據(jù)的頂部需要懸停,并且懸停的View要與ItemView中同樣布局的View進行操作同步,也就是相互同步兼贡。大家都知道,Android中有"The specified child already has a parent. You must call removeView() on the child's parent first."這個異常娃胆,意味著同一個View對象不能有兩個Parent遍希。我們就不能簡單粗暴的將同一個View對象添加進兩個parent了,需要另謀出路里烦。
方案選擇:
①sitckyScrollView懸停:不支持list的復用凿蒜,主線程會卡頓,pass胁黑。
②在listview的頂部覆蓋一個View废封,重新生成需要懸停的View,并做到懸停View和原View的同步丧蘸。針對listview的滑動和分組懸停漂洋,這個方案工作量太大,可行性不高。
③NestedScrollingChild和NestedScrollingParent方案刽漂,不支持分組演训,不合適。
④recyclerView + ItemDecoration方案 + View.draw(canvas) + motionEvent.offLocation():可行贝咙。
我為什么選擇第四個方案呢样悟?主要原因如下:
首先我們操作的是個列表,在控制懸停的View的顯示和移動時必須要知道頂部的Item的信息庭猩,RecyclerView.ItemDecoration可以很好的解決這個問題窟她。在ItemDecoration中可以輕松獲取到RecyclerView、可見的position以及RecyclerView.Adapter中的可見View等信息眯娱,這樣我們獲取到需要懸停的View就很容易了礁苗。
第二,在ItemDecoration#onDrawView( )方法中我們可以將需要懸停的View繪制出來徙缴。
ItemDecoration輕松幫我們實現(xiàn)了懸停View的繪制试伙,我們只需要處理真實View與懸繪制出來的懸停View的狀態(tài)同步即可。至于如何實現(xiàn)狀態(tài)同步于样,這個問題留待后面再說明疏叨。
ItemDecoration
這里先說下ItemDecoration的實現(xiàn),它是一個接口穿剖,內(nèi)部各個方法的作用如下圖所示:
如上所述蚤蔓,我們繪制View的時機應該是在onDrawOver方法中。
如何頂部的View
先上代碼糊余,
//獲取最頂部的ItemView
View adapterView = parent.getChildAt(0);
if (adapterView != null) {
//獲取需要繪制的View秀又,這里我們需要繪制的包括一個title,一個NewCHLayoutUnScroll的Header贬芥。
//順便獲取這兩個View的高度吐辙,后面我們需要他們的高度來實現(xiàn)懸停View異動的效果
View title = adapterView.findViewById(R.id.title);
int titleHeight = 0;
if (title != null && View.VISIBLE == title.getVisibility()) {
titleHeight = title.getMeasuredHeight();
}
int saveCount = canvas.save();
//設置總體偏移量,需要用到我們上邊獲取到的高度
stickyViewHeight = titleHeight;
if (adapterView.getBottom() < stickyViewHeight) {
offsetY = stickyViewHeight - adapterView.getBottom();
canvas.translate(0, -offsetY);
}
//渲染View
if (title != null) {
title.draw(canvas);
isTitleDrawed = true;
}
canvas.restoreToCount(saveCount);
} else {
}
接下來對上述代碼進行說明:
1蘸劈、獲取最頂部的ItemView
我們知道昏苏,RecyclerView#getChildren方法可以獲取到當前所有可見的ItemView,同理威沫,RecyclerView#getChildAt(int index)就可以根據(jù)position獲取到對應位置的View贤惯,這里我們就可以通過View adapterView = parent.getChildAt(0)來獲取到最頂部的ItemView了。
2棒掠、獲取需要繪制的View
我們需要繪制在頂部的View是最頂部的ItemView的子View孵构,根據(jù)view.findviewById(id)就可以獲取到需要繪制的View了。
3烟很、為了實現(xiàn)豎直方向RecyclerView時懸停的View同步上下滑動的效果颈墅,我們需要找到懸停View顯示完全與不完全的臨界值棒假,如下圖所示:
[圖片上傳失敗...(image-129a6f-1562746137143)]
[圖片上傳失敗...(image-d96ff2-1562746137143)]
如上圖所分析,在繪制懸停View時精盅,我們可以根據(jù)懸停View的高度和最上方ItemView.getBottom( )的大小來確定懸停View繪制的offset帽哑,從而就可以實現(xiàn)懸停View在合適的時機跟隨RecyclerView滑動。
將ItemView中的狀態(tài)變化同步給懸停View叹俏。
這里說的狀態(tài)變化同步主要包括itemView中的列表左右滑動和ItemView中的title的點擊事件觸發(fā)的懸停View的狀態(tài)更新妻枕。實現(xiàn)起來其實很簡單,只需要在ItemView中更新狀態(tài)時調(diào)用下面這行代碼即可:
mRecyclerView.invalidateItemDecorations();
RecyclerView#invalidateItemDecorations( )方法會引起ItemDecoration的重繪粘驰,onDrawOver方法勢必會重新調(diào)用屡谐,所以懸停View也就會重新繪制,就會跟頂部ItemView的title保持一致蝌数。
將懸停View的事件同步給頂部的ItemView愕掏。
這一步驟是最棘手的一步,這個問題可以理解為如何將canvas繪制的View的事件同步到被繪制的View上去顶伞。首先繪制出來的懸停View并不是真正的View饵撑,它的事件默認是傳遞給RecyclerView的,即使在RecyclerView中直接攔截了這個事件唆貌,如何處理也是個問題滑潘,因為很難定位MotionEvent的實際位置。
到了這一步锨咙,我們就可以借鑒前面提到過的StickyScrollView中對繪制出的懸停View的處理方法了语卤,核心代碼如下所示:
StickyScrollView#onTouchEvent
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (redirectTouchesToStickyView) {
ev.offsetLocation(0, ((getScrollY() + stickyViewTopOffset) - getTopForViewRelativeOnlyChild(currentlyStickingView)));
}
...
return super.onTouchEvent(ev);
}
核心代碼是ev.offsetLocation( ),我們看下它的源碼:
MotionEvent#offsetLocation
/**
* Adjust this event's location.
* @param deltaX Amount to add to the current X coordinate of the event.
* @param deltaY Amount to add to the current Y coordinate of the event.
*/
public final void offsetLocation(float deltaX, float deltaY) {
if (deltaX != 0.0f || deltaY != 0.0f) {
nativeOffsetLocation(mNativePtr, deltaX, deltaY);
}
}
這個方法會將MotionEvent的作用位置偏移一定的位置酪刀,也就是說會將事件傳遞到別的位置上粹舵。另外,在ViewGroup的事件分發(fā)的源碼中骂倘,也是通過MotionEvent#offsetLocation(offsetX, offsetY)來對事件進行處理的眼滤。
通過以上分析,我們可以通過MotionEvent#offsetLocation(offsetX, offsetY)方法將懸停View的MotionEvent傳遞給真實的View區(qū)域即可稠茂,唯一需要做的就是計算offsetY的值柠偶。
還有一個環(huán)節(jié)需要注意情妖,我們在哪兒獲取到這個MotionEvent睬关,如何獲取到RecyclerView.Item的事件呢?請看這兒毡证,Passing MotionEvents from RecyclerView.OnItemTouchListener to GestureDetectorCompat电爹,首先給recyclerView添加OnItemTouchListener,然后在OnItemTouchListener#onInterceptTouchEvent方法中就可以獲取到事件了料睛;獲取到事件之后我們還需要借助手勢相關的類來對事件進行處理丐箩。
具體代碼如下:
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.myfrag, container, false);
detector = new GestureDetectorCompat(getActivity(), new RecyclerViewOnGestureListener());
recyclerView = (RecyclerView) rootView.findViewById(R.id.recyclerview);
layoutManager = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(layoutManager);
recyclerView.addOnItemTouchListener(this);
adapter = new MyAdapter(myData));
recyclerView.setAdapter(adapter);
return rootView;
}
private class RecyclerViewOnGestureListener extends SimpleOnGestureListener {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
View view = recyclerView.findChildViewUnder(e.getX(), e.getY());
int position = recyclerView.getChildPosition(view);
// handle single tap
return super.onSingleTapConfirmed(e);
}
public void onLongPress(MotionEvent e) {
View view = recyclerView.findChildViewUnder(e.getX(), e.getY());
int position = recyclerView.getChildPosition(view);
// handle long press
super.onLongPress(e);
}
}
@Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
detector.onTouchEvent(e);
return false;
}
@Override
public void onTouchEvent(RecyclerView rv, MotionEvent e) {
}
好了摇邦,上面分析了如何實現(xiàn)類似IOS的分組懸停效果,對解決這個問題的思路進行了闡述屎勘,這里大致總結(jié)下:
參考:
2、靈感來源
4丑慎、手勢檢測