Android實(shí)踐之ScrollView中滑動(dòng)沖突處理

轉(zhuǎn)載注明出處:http://www.reibang.com/p/87a41b8c0dd0

前言

在Android開(kāi)發(fā)中,如果是一些簡(jiǎn)單的布局蹦哼,都很容易搞定,但是一旦涉及到復(fù)雜的頁(yè)面攘乒,特別是為了兼容小屏手機(jī)而使用了ScrollView以后设预,就會(huì)出現(xiàn)很多點(diǎn)擊事件的沖突,最經(jīng)典的就是ScrollView中嵌套了ListView碎乃。我想大部分剛開(kāi)始接觸Android的同學(xué)們都踩到過(guò)這個(gè)坑姊扔,這一篇文章就從最近做的一個(gè)項(xiàng)目講起,然后在過(guò)程中提供一些解決沖突的思路梅誓。

項(xiàng)目起始

項(xiàng)目有一個(gè)頁(yè)面恰梢,涉及到了ViewPager,MapView梗掰,ListView嵌言,也就是說(shuō)在一個(gè)頁(yè)面中,會(huì)有這三個(gè)View及穗,很明顯摧茴,屏幕無(wú)法完全顯示,需要ScrollView來(lái)做一下支援埂陆,就引入了ScrollView作為外層的容器苛白。但是由于這個(gè)頁(yè)面的數(shù)據(jù)展示需要做到用戶手動(dòng)下拉刷新,于是又引入了官方的SwipeRefreshLayout焚虱。

于是這個(gè)頁(yè)面的布局就成了這樣子购裙。如下圖(細(xì)節(jié)布局忽略)。


布局圖.png

加入了ScrollView和SwipeRefreshLayout之后引入了新的問(wèn)題鹃栽,就是各個(gè)控件之間的事件沖突躏率,嵌套在ScrollView中的ViewPager、MapView、ListView都需要能夠正確的處理點(diǎn)擊事件禾锤,特別是ListView私股,需求要求它在ScrollView中可以滑動(dòng),兩種滑動(dòng)混淆在一起恩掷,不是特別好處理倡鲸。

問(wèn)題提出來(lái)了,下面直接看解決思路黄娘。

解決滑動(dòng)沖突的思路

在ViewGroup中有個(gè)方法叫requestDisallowInterceptTouchEvent(boolean disallowIntercept)峭状,這個(gè)方法可以用來(lái)控制該ViewGroup是否截?cái)帱c(diǎn)擊事件。我們解決滑動(dòng)沖突的時(shí)候逼争,其實(shí)就是在某個(gè)時(shí)機(jī)去調(diào)用這個(gè)方法优床,讓父布局不截?cái)帱c(diǎn)擊事件,將點(diǎn)擊事件傳遞到子View誓焦,讓相關(guān)的子View去處理胆敞。

下面就是關(guān)于在項(xiàng)目中處理各種點(diǎn)擊事件沖突的一些例子和思考。處理的方法只是提供一種思路杂伟,可能并不是最優(yōu)的方法移层,肯定存在其他思路的解決方案。

以下處理滑動(dòng)沖突的方案都是在子View的OnTouchListener里面進(jìn)行處理赫粥,并沒(méi)有去復(fù)寫(xiě)控件的點(diǎn)擊事件處理過(guò)程观话,在使用中還是比較方便的。

MapView地圖頁(yè)面滑動(dòng)沖突

MapView與ScrollView的沖突主要在于越平,當(dāng)用戶點(diǎn)擊到MapView地圖并且滑動(dòng)的時(shí)候频蛔,希望由地圖Map去處理點(diǎn)擊事件,并包括后續(xù)的滑動(dòng)事件秦叛、雙手指縮放地圖等等晦溪。

在ScrollView中,是會(huì)默認(rèn)截?cái)帱c(diǎn)擊事件的挣跋,導(dǎo)致用戶點(diǎn)擊到地圖后尼变,地圖基本是沒(méi)有反應(yīng),更別談雙手指縮放地圖了浆劲。

用戶手指點(diǎn)擊到地圖,并且滑動(dòng)的時(shí)候哀澈,很難確定用戶是要ScrollView上下滑動(dòng)還是操控地圖內(nèi)容滑動(dòng)牌借,所以我簡(jiǎn)單的認(rèn)為,只要用戶手指點(diǎn)擊到地圖割按,就是要對(duì)地圖進(jìn)行操作膨报;當(dāng)用戶手指抬起,就認(rèn)為用戶不需要操作地圖了。

解決思路也很簡(jiǎn)單现柠,就是在用戶點(diǎn)擊到地圖或者滑動(dòng)地圖時(shí)候院领,讓ScrollView不截?cái)帱c(diǎn)擊事件,并傳遞給子View處理够吩,也就是地圖去處理點(diǎn)擊事件比然;當(dāng)用戶手指抬起的時(shí)候,將ScrollView的狀態(tài)恢復(fù)至之前的狀態(tài)周循,也就是ScrollView可以截?cái)帱c(diǎn)擊事件强法。

我使用的是百度地圖,直接上代碼湾笛,更容易理解饮怯。

mMapView.getChildAt(0).setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if(event.getAction() == MotionEvent.ACTION_UP){
                    //允許ScrollView截?cái)帱c(diǎn)擊事件,ScrollView可滑動(dòng)
                    mScrollView.requestDisallowInterceptTouchEvent(false);
                }else{
                    //不允許ScrollView截?cái)帱c(diǎn)擊事件嚎研,點(diǎn)擊事件由子View處理
                    mScrollView.requestDisallowInterceptTouchEvent(true);
                }
                return false;
            }
        });
ViewPager滑動(dòng)沖突解決

在這個(gè)項(xiàng)目中蓖墅,ViewPager在頁(yè)面最頂層,如果只是ScrollView里面嵌套了ViewPager临扮,因?yàn)檫@兩個(gè)控件是不同方向的滑動(dòng)事件论矾,所以基本不會(huì)出現(xiàn)沖突。

但是由于引入了SwipeRefreshLayout公条,我發(fā)現(xiàn)在滑動(dòng)ViewPager的時(shí)候拇囊,很容易就觸發(fā)了SwipeRefreshLayout的下來(lái)刷新,進(jìn)而有可能阻斷了ViewPager的左右滑動(dòng)效果靶橱,體驗(yàn)很不好寥袭。而且在滑動(dòng)ViewPager的過(guò)程中,用戶滑動(dòng)肯定不是一直水平的关霸,會(huì)有一定程度向上向下的滑動(dòng)传黄。

ViewPager處理沖突和地圖處理沖突有些不同,因?yàn)楫?dāng)用戶點(diǎn)擊到ViewPager队寇,在滑動(dòng)過(guò)程中膘掰,基本就可以猜測(cè)到用戶是想左右滑動(dòng)ViewPager還是上下滑動(dòng)ScrollView(或者下拉刷新),這就不能像地圖一樣佳遣,在點(diǎn)擊到ViewPager就禁止ScrollView截?cái)帱c(diǎn)擊事件(或者SwipeRefreshLayout下拉刷新功能)之宿,需要在滑動(dòng)過(guò)程中做出判斷。

解決思路就是钾军,設(shè)定一個(gè)閾值嘲驾,一旦用戶在X軸也就是橫向滑動(dòng)距離超過(guò)這個(gè)閾值,我就認(rèn)為用戶是要左右滑動(dòng)ViewPager诵盼,就禁止ScrollView截?cái)帱c(diǎn)擊事件同時(shí)設(shè)置SwipeRefreshLayout不能下拉刷新惠豺。當(dāng)用戶抬起手指银还,就認(rèn)為用戶對(duì)ViewPager的操作已經(jīng)完畢,將ScrollView和SwipeRefreshLayout狀態(tài)恢復(fù)洁墙。

mViewPager.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        int action = event.getAction();

        if(action == MotionEvent.ACTION_DOWN) {
            // 記錄點(diǎn)擊到ViewPager時(shí)候蛹疯,手指的X坐標(biāo)
            mLastX = event.getX();
        }
        if(action == MotionEvent.ACTION_MOVE) {
            // 超過(guò)閾值
            if(Math.abs(event.getX() - mLastX) > 60f) {
                mRefreshLayout.setEnabled(false);
                mScrollView.requestDisallowInterceptTouchEvent(true);
            }
        }
        if(action == MotionEvent.ACTION_UP) {
            // 用戶抬起手指,恢復(fù)父布局狀態(tài)
            mScrollView.requestDisallowInterceptTouchEvent(false);
            mRefreshLayout.setEnabled(true);
        }
        return false;
    }
});

用戶點(diǎn)擊到ViewPager時(shí)候热监,記錄下點(diǎn)擊位置的X坐標(biāo)捺弦,當(dāng)用戶滑動(dòng)過(guò)程中,如果在X軸上面的滑動(dòng)超過(guò)閾值(我寫(xiě)的是60f狼纬,這個(gè)可以在實(shí)際使用中自行設(shè)置最佳的閾值)羹呵,就禁止ScrollView截?cái)帱c(diǎn)擊事件,同時(shí)設(shè)置不可下拉刷新疗琉。當(dāng)用戶手指離開(kāi)屏幕冈欢,將ScrollView和SwipeRefreshLayout的狀態(tài)恢復(fù)。

ListView滑動(dòng)沖突解決

在ScrollView中嵌套ListView盈简,會(huì)出現(xiàn)各種各樣奇怪的問(wèn)題凑耻。比如說(shuō)ListView顯示有問(wèn)題,可能才一兩個(gè)Item那么高柠贤,并沒(méi)有完全的展開(kāi)香浩。網(wǎng)上流傳解決這種問(wèn)題的方法會(huì)有兩種。

  • 根據(jù)展示數(shù)據(jù)的個(gè)數(shù)乘以每一個(gè)Item的高度臼勉,計(jì)算出ListView的總體高度邻吭,然后動(dòng)態(tài)的設(shè)置ListView的高度
  • 復(fù)寫(xiě)ListView的onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法,讓ListView完全展開(kāi)

這兩種方法都可以解決ListView展示不完全的問(wèn)題宴霸,而且也可以滑動(dòng)(其實(shí)是使用ScrollView的滑動(dòng)效果)囱晴,但是有一個(gè)最大的遺憾,就是ListView里面的View不能復(fù)用了瓢谢。因?yàn)檫@兩種方法都是算出了ListView的全部高度畸写,然后將ListView控件的高度設(shè)置成這個(gè)高度,這樣的話氓扛,ListView就相當(dāng)于一個(gè)LinearLayout的布局了枯芬,失去了復(fù)用View的優(yōu)勢(shì),而且在某些場(chǎng)景可能還沒(méi)有LinearLayout好用采郎,更甚的是千所,如果有大量圖片的話,很容易就OOM了蒜埋,這是在研發(fā)過(guò)程中最不希望看見(jiàn)的淫痰。

可以參考一下美團(tuán),美團(tuán)的首頁(yè)理茎,就是一個(gè)ScrollView黑界,下滑的時(shí)候會(huì)發(fā)現(xiàn),并不能無(wú)限向下滑動(dòng)皂林,到了底部會(huì)提醒跳轉(zhuǎn)到一個(gè)二級(jí)頁(yè)面去查看全部的團(tuán)購(gòu)信息朗鸠。這是處理ScrollView里面嵌套類似ListView列表布局的時(shí)候的一種解決方案。

但是在我遇見(jiàn)的這個(gè)項(xiàng)目里面础倍,并不能這樣處理烛占。

上面的提到的兩種解決思路很明確,如果想要ListView正常展示就需要確定ListView的高度沟启,這個(gè)很重要忆家。

所以首先,我需要在布局文件中設(shè)置ListView的高度德迹,是一個(gè)明確的數(shù)值芽卿。設(shè)置高度之后,如果ListView中的數(shù)據(jù)的Item總高度超過(guò)ListView所設(shè)置的高度胳搞,就可以復(fù)用View了卸例。但是這只是解決了ListView的顯示問(wèn)題,ListView與ScrollView的滑動(dòng)沖突肌毅,并沒(méi)有解決筷转。

要解決滑動(dòng)的沖突,最主要的是確定禁止ScrollView截?cái)帱c(diǎn)擊事件的時(shí)機(jī)悬而,然后來(lái)分析有哪些時(shí)機(jī)呜舒。

  • ScrollView在未滑動(dòng)到底部時(shí)候,如果點(diǎn)擊并滑動(dòng)ListView時(shí)候笨奠,ListView是不能滑動(dòng)的袭蝗,不禁止。
  • 如果ScrollView滑動(dòng)到底部艰躺,且ListView已經(jīng)到頂部呻袭,繼續(xù)下拉ListView,其實(shí)會(huì)拉動(dòng)ScrollView腺兴,不禁止左电。
  • 如果ScrollView滑動(dòng)到底部,用戶向上滑页响,ListView滑動(dòng)篓足,禁止ScrollView截?cái)帱c(diǎn)擊事件能力

很明顯,在判斷禁止ScrollView截?cái)帱c(diǎn)擊事件時(shí)機(jī)的時(shí)候闰蚕,需要知道ScrollView是否滑動(dòng)到了底部栈拖。于是,重寫(xiě)了ScrollView的ScrollChanged()方法没陡,來(lái)判斷ScrollView是否滑動(dòng)到底部(SDK API 23版本中ScrollView可以設(shè)置setOnScrollChangeListener()來(lái)監(jiān)聽(tīng)滑動(dòng)的變化涩哟,但是之前版本不支持索赏,為了兼容,自己需要重寫(xiě))贴彼。

@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt){
    super.onScrollChanged(l,t,oldl,oldt);
    // 滑動(dòng)的距離加上本身的高度與子View的高度對(duì)比
    if(t + getHeight() >=  getChildAt(0).getMeasuredHeight()){
        // ScrollView滑動(dòng)到底部
        if(mOnScrollToBottomListener != null) {
            mOnScrollToBottomListener.onScrollToBottom();
        }
    } else {
        if(mOnScrollToBottomListener != null) {
            mOnScrollToBottomListener.onNotScrollToBottom();
        }
    }
}

public void setScrollToBottomListener(OnScrollToBottomListener listener) {
    this.mOnScrollToBottomListener = listener;
}

public interface OnScrollToBottomListener {
    void onScrollToBottom();
    void onNotScrollToBottom();
}

有了思路潜腻,而且ScrollView滑動(dòng)到底部的標(biāo)識(shí)也可以拿到,下面就可以直接來(lái)解決滑動(dòng)沖突了器仗,直接看代碼融涣。

mScrollView.setScrollToBottomListener(new BottomScrollView.OnScrollToBottomListener() {
    @Override
    public void onScrollToBottom() {
        isSvToBottom = true;
    }

    @Override
    public void onNotScrollToBottom() {
        isSvToBottom = false;
    }
});

mListView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        int action = event.getAction();

        if(action == MotionEvent.ACTION_DOWN) {
            mLastY = event.getY();
        }
        if(action == MotionEvent.ACTION_MOVE) {
            int top = mListView.getChildAt(0).getTop();
            float nowY = event.getY();
            if(!isSvToBottom) {
                // 允許scrollview攔截點(diǎn)擊事件, scrollView滑動(dòng)
                mScrollView.requestDisallowInterceptTouchEvent(false);
            } else if(top == 0 && nowY - mLastY > THRESHOLD_Y_LIST_VIEW) {
                // 允許scrollview攔截點(diǎn)擊事件, scrollView滑動(dòng)
                mScrollView.requestDisallowInterceptTouchEvent(false);
            } else {
                // 不允許scrollview攔截點(diǎn)擊事件, listView滑動(dòng)
                mScrollView.requestDisallowInterceptTouchEvent(true);
            }
        }
        return false;
    }
});

相對(duì)于其他的控件來(lái)說(shuō)精钮,ListView和ScrollView之間的滑動(dòng)沖突更難解決威鹿,但其實(shí)在實(shí)際使用中并不推薦ScrollView里面嵌套ListView,一旦業(yè)務(wù)復(fù)雜轨香,很容易出現(xiàn)各種UI和業(yè)務(wù)邏輯沖突的錯(cuò)誤忽你。

運(yùn)行效果

由于地圖加入比較麻煩,所以在Demo中并沒(méi)有引入地圖弹沽√醇校看一下運(yùn)行效果。


運(yùn)行效果

總結(jié)

本篇文章只是提供一種解決方法的思路策橘,在具體的場(chǎng)景下炸渡,交互往往是貼合具體業(yè)務(wù)需求的。但是不管怎么樣丽已,找出點(diǎn)擊事件截?cái)嗪吞幚淼臅r(shí)機(jī)是最重要的蚌堵,圍繞這個(gè)關(guān)鍵點(diǎn),總能找出相應(yīng)的解決方法沛婴。

附上Demo工程地址:Demo工程地址鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末吼畏,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子嘁灯,更是在濱河造成了極大的恐慌泻蚊,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件丑婿,死亡現(xiàn)場(chǎng)離奇詭異性雄,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)羹奉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)秒旋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人诀拭,你說(shuō)我怎么就攤上這事迁筛。” “怎么了耕挨?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵细卧,是天一觀的道長(zhǎng)尉桩。 經(jīng)常有香客問(wèn)我,道長(zhǎng)贪庙,這世上最難降的妖魔是什么魄健? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮插勤,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘革骨。我一直安慰自己农尖,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布良哲。 她就那樣靜靜地躺著盛卡,像睡著了一般。 火紅的嫁衣襯著肌膚如雪筑凫。 梳的紋絲不亂的頭發(fā)上滑沧,一...
    開(kāi)封第一講書(shū)人閱讀 51,688評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音巍实,去河邊找鬼滓技。 笑死,一個(gè)胖子當(dāng)著我的面吹牛棚潦,可吹牛的內(nèi)容都是我干的令漂。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼丸边,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼叠必!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起妹窖,我...
    開(kāi)封第一講書(shū)人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤纬朝,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后骄呼,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體共苛,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年谒麦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了俄讹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡绕德,死狀恐怖患膛,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情耻蛇,我是刑警寧澤踪蹬,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布胞此,位于F島的核電站,受9級(jí)特大地震影響跃捣,放射性物質(zhì)發(fā)生泄漏漱牵。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一疚漆、第九天 我趴在偏房一處隱蔽的房頂上張望酣胀。 院中可真熱鬧,春花似錦娶聘、人聲如沸闻镶。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)铆农。三九已至,卻和暖如春狡耻,著一層夾襖步出監(jiān)牢的瞬間墩剖,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工夷狰, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留岭皂,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓沼头,卻偏偏與公主長(zhǎng)得像蒲障,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子瘫证,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容