解決原生橫滑scrollView和Flutter 縱滑CustomScroll的沖突

前言


最近項目中在橫向滑動的PageViewController(WMPageController)里嵌入了Flutter 的 CustomScrollView,原本會覺得一切ok,卻出現(xiàn)了一個致命的問題:安卓嵌入之后滑動流程穩(wěn)定,沒有任何問題,iOS 嵌入之后出現(xiàn)Flutter 頁面滑動卡頓、不流暢,體現(xiàn)在觸發(fā)flutter 列表滑動的同時,會觸發(fā)原生橫滑,且必須垂直滑動(無一點左右的滑動偏移)才會穩(wěn)定觸發(fā)Flutter 列表的滑動,否則容易觸發(fā)左右橫滑,所以我們習(xí)慣了單手用手機的人,根本無法順暢滑動。如下面的gif(其實我已經(jīng)很努力的在上下滑了,很明顯上下滑動的距離大于水平滑動的距離,用戶的用意也應(yīng)該是要上下滑動,但是卻觸發(fā)了橫滑):

1595228716860927.png

解決方式


首先觸摸flutter的頁面,原生的UIScrollView的panGesture也會收到觸摸事件,導(dǎo)致UIScrollView的手勢生效,開始我想著如果能再UIScrollView的手勢代理方法中區(qū)分出Flutter CustomScrollView的手勢,然后再做調(diào)整。但是這條路是錯誤的,因為Flutter的滑動手勢并不是采用系統(tǒng)提供的手勢,在斷點時發(fā)現(xiàn),觸發(fā)之后,并沒有相關(guān)flutter 上的 系統(tǒng)手勢類。所以我改變了思路用以下去解決這個問題:

  1. 如果當(dāng)前觸摸在FlutterView上,我們的子類UIScrollView 手勢就不接收touch事件,控制其手勢不會生效;
  2. 繼承FLBFlutterViewContainer父類,重寫touches相關(guān)方法,手動調(diào)整UIScrollView 的contentOffset(注意: 我們混合棧使用的是閑魚的FlutterBoost, FLBFlutterViewContainer繼承自FlutterViewController)

上代碼:

  1. 自定義繼承自UIScrollView的子類,并且實現(xiàn)手勢代理方法,控制Touch的傳遞。如果touch的View是FlutterView 我們的ScrollView就不接收這個touch事件。

    @interface WMScrollView()
    
    @end
    
    @implementation WMScrollView
    
    #pragma mark - <UIGestureRecognizerDelegate>
    
    - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
        if ([NSStringFromClass(touch.view.class) isEqualToString:@"FlutterView"]) {
            return NO;
        }
        return YES;
    }
    
    @end
    
  1. 自定義一個繼承自FLBFlutterViewContainer的子類,重寫Touches方法,在touchBegan方法的時候,我們讓自定的ScrollView手勢不生效;在touchMoved的時候,我們干預(yù)計算觸摸方向的識別,如果在縱滑<5 && 橫滑大于2的時候我們默認用戶想要橫滑 橫滑距離大于縱滑的距離我們認為用戶想要橫滑(判斷橫滑距離大于縱滑距離如下代碼),故手動改變ScrollView的contentOffset,否則的話手勢繼續(xù)不生效, flutterView 生效;在touchEnd和touchCancel的情況下,我們判斷當(dāng)前的contentOffset,如果距離大于當(dāng)前頁距離的1/3, 跳轉(zhuǎn)到下一頁,如果距離小于上一頁距離的2/3, 跳轉(zhuǎn)到前一頁;負責(zé)調(diào)整offset回到當(dāng)前頁面的起始位置。

    @implementation DDScrollFlutterViewContainer
    
    - (void)viewDidLoad {
        [super viewDidLoad];
     }
    
    #pragma mark - UITouches
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        [super touchesBegan:touches withEvent:event];
        [self touchesBegan:touches];
    }
    
    - (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        [self touchMoveWithTouch:touches event:event];
    }
    
    - (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        [self touchEnd:touches event:event isCanceled:NO];
    }
    
    - (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        [self touchEnd:touches event:event isCanceled:YES];
    }
    
    - (void)touchesBegan:(NSSet *)touches {
        UIScrollView *superView = (UIScrollView *)self.view.superview.superview;
        superView.panGestureRecognizer.enabled = NO;
        self.hasOffset = NO;
        self.lastLocation = [touches.anyObject locationInView:self.view];
    }
    
    - (void)touchMoveWithTouch:(NSSet *)touches event:(UIEvent *)event{
        UITouch *touch = [touches anyObject];
        CGPoint previoursLocation = self.lastLocation;
        CGPoint location = [touch locationInView:self.view];
        CGFloat deltax = fabs(location.x - previoursLocation.x);
        CGFloat deltay = fabs(location.y - previoursLocation.y);
        UIScrollView *superView = (UIScrollView *)self.view.superview.superview;
        if (deltax > deltay) {
            CGPoint offset = superView.contentOffset;
            [superView.delegate scrollViewWillBeginDragging:superView];
            CGPoint previours = [touch previousLocationInView:self.view];
            [superView setContentOffset:CGPointMake(offset.x - (location.x - previours.x), offset.y)];
            self.hasOffset = YES;
        } else {
            if (!self.hasOffset) {
                [super touchesMoved:touches withEvent:event];
            }
        }
    }
    
    - (void)touchEnd:(NSSet *)touches event:(UIEvent *)event isCanceled:(BOOL)isCanceled {
        UIScrollView *superView = (UIScrollView *)self.view.superview.superview;
        if (self.hasOffset) {
            CGPoint offset = superView.contentOffset;
            CGFloat offsetX = offset.x;
            if (offsetX > (1 * self.view.width+ self.view.width * 1 / 3)) {
                [superView setContentOffset:CGPointMake(2*self.view.width, offset.y) animated:YES];
            } else if (offsetX <=  self.view.width * 2 / 3) {
                [superView setContentOffset:CGPointMake(0, offset.y) animated:YES];
            } else {
                [superView setContentOffset:CGPointMake(self.view.width, offset.y) animated:YES];
            }
            [superView.delegate scrollViewDidEndDecelerating:superView];
            [self.nextResponder touchesEnded:touches withEvent:event];
        }
    isCanceled ? [super touchesCancelled:touches withEvent:event] : [super touchesEnded:touches withEvent:event];
        superView.panGestureRecognizer.enabled = YES;
    }
    
    
    @end
    

處理之后的效果:

flutter_and_ios_matter_sh2.gif

總結(jié)


這個問題也是困擾了許久,因為在安卓上表現(xiàn)良好,而在iphone上表現(xiàn)極其的不舒適,所以最初考慮到這也是Flutter團隊對于CustomScrollView 原生渲染適配的問題,但是既然他們適配不好,我們就需要手動的去適配。但是從解決辦法的探索中,我們發(fā)現(xiàn)Flutter的手勢并不是通過iOS原生的手勢實現(xiàn)的,而是自己的一套手勢識別。而且FlutterViewController 的touches事件是被重寫了,因為假如你重寫了touches的方法,沒有調(diào)用父類方法,那么會發(fā)現(xiàn)flutter 不能滑動,所以判斷flutter的滑動事件Touches方法也是參與其中,所以記得調(diào)用父類方法。而我們重寫了Touches的事件也是區(qū)分了一下用戶意圖要滑動的方向,相當(dāng)于做了個手勢的方向的識別。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容