請尊重個人勞動成果,轉載注明出處,謝謝!
http://blog.csdn.net/amazing7/article/details/51768942
先看 SwipeLayout的效果圖
圖太多了,我這只上傳1張,想看 listview和GridView效果的,和想看源碼的 —> GitHub
怎樣實現后面說,先說會空話。
最近整理之前的項目,那時有1個這樣的需求,要在ExpandableListView的ChildView上實現 編輯和刪除的側滑菜單。我當時并沒有用他人的框架,實現出來大概是這個模樣。
滑動事件完好,沒有沖突,子view可以取得點擊事件。
實現起來非常簡單,大概分為3步:
①、這個Item布局采取HorizontalScrollView來作為最外層布局(對,就是它,它可以橫向轉動),注意 HorizontalScrollView繼承的是FrameLayout,意味著只能有1個子布局。在這個子布局里,擺放兩個子子布局,left用于顯示內容,right顯示菜單。
②、初始化中獲得屏幕寬度
DisplayMetrics dm = new DisplayMetrics();
getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm);
mScreentWidth = dm.widthPixels;
在Adapter的getChildView方法中設置leftView的寬度為屏幕寬度
// 設置leftView的大小為屏幕寬度,這樣右側的rightView就正好被擠出屏幕外
holder.leftview= convertView.findViewById(R.id.left);
LayoutParams lp = holder.leftview.getLayoutParams();
lp.width = mScreentWidth;
③、getChildView方法中給convertView設置Touch監聽事件
convertView.setOnTouchListener(new View.OnTouchListener()
{
@Override
public boolean onTouch(View v, MotionEvent event)
{
switch (event.getAction())
{
case MotionEvent.ACTION_DOWN:
if (view != null) {
//有view被滑開了,點擊其他childview時,用于還原
ViewHolder viewHolder = (ViewHolder) view.getTag();
viewHolder.hsv.smoothScrollTo(0, 0);
}
break;
case MotionEvent.ACTION_UP:
ViewHolder viewHolder = (ViewHolder) v.getTag();
view = v;
// 取得HorizontalScrollView滑動的水平方向值.
int scrollX = viewHolder.hsv.getScrollX();
int rightW = viewHolder.rightView.getWidth();
if (scrollX < rightW / 4)
{ //滑動距離小于右側布局的1/4收縮
viewHolder.hsv.smoothScrollTo(0, 0);
}else
{ //展開
viewHolder.hSView.smoothScrollTo(rightW, 0);
}
break;
}
return true;
}
});
// 刪除1條后更新狀態
if (holder.hsv.getScrollX() != 0) {
holder.hsv.scrollTo(0, 0);
}
簡單說1下,HorizontalScrollView在dispatchTouchEvent的時候,如果發現時橫向滑動就把事件交給onTouchEvent處理,而這個onTouchEvent方法是來自view的(viewGroup也是調用view的該方法),在view的dispatchTouchEvent中,是順序調用OnTouchListener和onTouchEvent的。我們這setOnTouchListener中自己處理了滑動事件,并且返回true,就消耗掉了事件,不會再調用onTouchEvent。
開始說主題,假定都對 view和viewGroup的事件分發機制 、自定義viewGroup 的流程 和 Scroller 都有了初步的了解。如果沒有,我們就假定有!
算了,還沒有了解可以看看 View和ViewGroup事件分發機制源碼分析 和 自定義FlowLayout實現標簽快捷輸入框 、Android Scroller大揭秘這3篇文章。
我是繼承LinearLayout實現的,為何不繼承HorizontalScrollView或是ViewGroup?
HorizontalScrollView其實就是1個實現轉動功能的FrameLayout,view的onMeasure和onLayout是層層實現的,我不能在HorizontalScrollView的onLayout方法中對其子view的子view直接設置為屏幕寬度。
繼承ViewGroup就要自己丈量和擺放子view,開甚么玩笑,我這么懶得人。
為了節省大家寶貴的時間,下面只說重點。
為何有事件沖突呢? 我們知道listview是上下滑動的,而我們的這個側滑布局要左右滑動。當我們屏幕上橫向滑動時,只要略微斜了1點,那末listview就認為要上下滑動,它就把滑動事件攔截了自己交給自己的onTouchEvent處理。根本都分發不到SwipeLayout的局部中。產生效果:側滑出來1點劃不動了,卡住了…
但是google早已看穿了1切,他們給我們提供了這個方法。
requestDisallowInterceptTouchEvent(true);
看過源碼的火伴肯定知道,這其實就是設置1個標志位。當傳入true時,駁回父view的中斷要求。( 就是父view不能中斷事件必須分發到子view。)
那末事件就由我們處理了,由于listview需要上下滑動事件,而SwipeLayout需要左右滑動事件,恰好各取所需,復寫dispatchTouchEvent(MotionEvent ev)方法來實現各取所需。
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
/**
* 不允許父view對觸摸事件的攔截
*/
disallowParentsInterceptTouchEvent(getParent());
startX = ev.getX();
startY = ev.getY();
isHorizontalMove =false;
break;
case MotionEvent.ACTION_MOVE:
if(!isHorizontalMove){
curX = ev.getX();
curY = ev.getY();
float dx = curX - startX;
float dy = curY - startY;
/**
* 認為產生了滑動
*/
if(dx*dx+dy*dy > mTouchSlop*mTouchSlop){
/**
* 垂直滑動
*/
if (Math.abs(dy) > Math.abs(dx)){
/**
* 允許父view對觸摸事件攔截,讓其他view去處理事件
*/
allowParentsInterceptTouchEvent(getParent());
/**
* 垂直轉動復原所有item
*/
shrinkAllView();
}else{
/**
* 水平滑動,攔截來自己處理
*/
isHorizontalMove = true;
/**
* 為了在onTouchEvent的Move事件中第1次摹擬滑動距離不要太大,
* 記錄上1次產生move的位置
*/
lastX = curX;
}
}
}
break;
default:
break;
}
return super.dispatchTouchEvent(ev);
}
在ACTION_DOWN中 駁回父view攔截,那末我們就能夠開心的在ACTION_MOVE對事件進行處理(先判斷是不是產生了滑動,再判斷是 上下 還是 左右,是上下就取消對父類的駁回,讓listview去處理事件,同時讓所有劃開的SwipeLayout關閉。是左右滑動就 中斷繼續往下層分發,攔截到自己的onTouchEvent做事件處理)。
中斷分發:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(isHorizontalMove){
/**
* 產生水平滑動,把事件中斷到本層onTouchEvent中處理
*/
return true;
}
return super.onInterceptTouchEvent(ev);
}
我這側滑菜單里是兩個imageView默許是不可clickable的,所以就算不攔截最后也會履行我的onTouchEvent處理。但是如果是Button,ImageButton等默許是可點擊的,那末事件就傳不上來了。
由于我們不知道是哪一個父view對事件進行了攔截,所以要循環遞歸設置標志位(為了更好的兼容性,反正我這是listview攔截了)。
/**
* 由于不知道是父view那1層會攔截觸摸事件,所以遞歸向上設置標志位
* 直到頂層view,就直接返回
*/
private void disallowParentsInterceptTouchEvent(ViewParent parent) {
if (null == parent) {
return;
}
parent.requestDisallowInterceptTouchEvent(true);
disallowParentsInterceptTouchEvent(parent.getParent());
}
private void allowParentsInterceptTouchEvent(ViewParent parent) {
if (null == parent) {
return;
}
parent.requestDisallowInterceptTouchEvent(false);
allowParentsInterceptTouchEvent(parent.getParent());
}
當標志位表明是橫向滑動時,我們需要在ACTION_MOVE里面摹擬轉動。觸發1次ACTION_MOVE就讓內容轉動1點點,實現效果就是內容跟隨手指移動。
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
if(isHorizontalMove){
curX = ev.getX();
float dX = curX-lastX;
/**
* 不斷更新lastX的位置,用于摹擬滑動
*/
lastX = curX;
/**
* 滑動的距離與實際相反,由于轉動的時候移動的是內容,不是view
*/
int disX = getScrollX() + (int)(-dX);
/**
* 手指向右移動
*/
if(disX<0){
/**
* 如果菜單收縮,避免越界(越界后ACTION_UP又會轉動回來,但還是不越界的好)
* 如果菜單展開,,我們希望迅速關閉菜單,不需要摹擬轉動
*/
scrollTo(0, 0);
}
/**
* 手指向左移動,如果累加的移動距離已大于menu的寬度,就讓menu顯示出來。
* 如果移動距離還不到,就摹擬轉動
*/
else if(disX>rightViewWidth){
scrollTo(rightViewWidth,0);
}
else{
scrollTo(disX, 0);
}
}
break;
case MotionEvent.ACTION_UP:
float endX = ev.getX();
float dis =endX -startX;
/**
* 手指向左滑動,摹擬展開
*/
if(dis<0){
SimulateScroll(EXPAND);
}
/**
* 手指向右滑動,摹擬關閉
*/
else{
SimulateScroll(SHRINK);
}
default:
break;
}
return true;
}
先說1下這個lastX,這個坐標是onInterceptTouchEvent中判斷為橫向移動后的最近坐標。(事件不能被消費掉,但是會隨著時間消失,我們可以在最內層到最外層的所有view的onTouchEvent中對1個事件進行處理,全部返回false。但是這里是 在onInterceptTouchEvent中已觸發了11個(大概)ACTION_MOVE事件,然后onTouchEvent才進行處理。簡單的說就是我滑動的前1段距離拿去做判斷了,判斷好了才跟這手指移動。)
那這么辦呢?要末
float dX = curX-lastX;
滑動順暢,不足的距離由最后scroller摹擬轉動補回。
float dX = curX-startX;
剛產生移動那1下移動約10個ACTION_MOVE事件的距離,效果不好。
給不了解的惡補1下概念:
getX() 和getY()是view的內部坐標,大小補回超太長寬。
getScrollX()
獲得的是view左上角到內容左上角的距離。至于正負表示方向。
Positive numbers will scroll the content to the left.
屏幕剛顯示,未產生移動之前getScrollX()等于0,每次scrollTo后會刷新getScrollX()的值。
在1次ACTION_MOVE事件中,手指移動了dx距離,那末讓內容也1起移動dx,就實現了跟隨手指移動的效果。
相當于 getScrollX()+=dx。
當我們手指拿起來的時候,要判斷SwipeLayout的菜單欄是應當收縮還是應當展開。當肯定了狀態后就要摹擬手指觸摸滑動到指定位置。
/**
* move事件里摹擬滑動完成后,判斷展開狀態
* 再摹擬轉動到目標位置
*/
public void SimulateScroll(int type){
int dx =0;
switch (type){
case EXPAND:
//手指向左滑動getScrollX為正
dx = rightViewWidth-getScrollX();
break;
case SHRINK:
//手指向右滑動getScrollX為負
dx = 0-getScrollX();
break;
default:
break;
}
scroller.startScroll(getScrollX(),0,dx,0,Math.abs(dx)/2);
invalidate();
}
@Override
public void computeScroll() {
/**
* Call this when you want to know the new location. If it returns true,
* the animation is not yet finished.
*
* 返回true代表正在摹擬數據,false 已停止摹擬數據
*/
if (scroller.computeScrollOffset()) {
/**
* 更新X軸的偏移量
*/
scrollTo(scroller.getCurrX(), 0);
/**
* 遞歸調用computeScroll()方法,直到摹擬轉動完成
*/
invalidate();
}
}
這里調用invalidate()重繪UI,會再次調用computeScroll(),遞歸 直到 摹擬轉動完成。
listview滑動時所有SwipeLayout復原。
我們知道listview刪除1個item,不1定會回收view,有可能只是重新裝載了數據。那末顯示的時候要SwipeLayout復原。
/**
* 用于上下滑動和刪除item時的,狀態改變
*/
static List<SwipeLayout> swipelayouts = new ArrayList<>();
public static void addSwipeView(SwipeLayout v){
if(null==v){
return;
}
swipelayouts.add(v);
}
public static void removeSwipeView(SwipeLayout v){
if(null==v){
return;
}
v.SimulateScroll(SwipeLayout.SHRINK);
}
private void shrinkAllView(){
for(SwipeLayout s :swipelayouts){
if(null==s){
swipelayouts.remove(s);
continue;
}else {
s.SimulateScroll(SwipeLayout.SHRINK);
}
}
}
內部定義了3個方法,當刪除item時調用removeSwipeView方法使該view復原。在adapte的getview方法添加新item時調用addSwipeView,把當前顯示的所有SwipeLayout都裝在這個list里面。在上面dispatchTouchEvent中判斷為上下滑動事件的時候調用shrinkAllView復原所有。
基本上就完了,有木有很簡單!哈哈
我們這里是解決了上下滑動和左右滑動的沖突,那末listview 中item為scrollview時,兩個都要豎著滑動。
我們為何想要在listview 中使用scrollview,由于我們的item中需要顯示的太多。我們知道當scrollview里的內容高度 小于它父view給他的高度的時候,它是完全展開的,不需要也不能滑動。那末使listview給item足夠大的高度,讓scrollview沒必要以轉動的方式來展現,就能夠解決這個滑動沖突,反正轉動事件也會被listview 攔截。
新建1個新的NoScrollListView繼承與listview,并復寫onMeasure方法。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//這好比你有1個炒雞有錢的爹,你想要多少都能滿足你,對! 是滿足你...
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 7, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
那末我有1個問題,viewpage+fragment+listview +SwipeLayout,我們知道viewpage要左右滑動,當產生滑動事件的時候,我是該移動SwipeLayout呢?還是讓viewpage子去翻頁呢?
下一篇 多線程下載文件