前言
最近項目中在橫向滑動的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ā)了橫滑):

解決方式
首先觸摸flutter的頁面,原生的UIScrollView的panGesture也會收到觸摸事件,導(dǎo)致UIScrollView的手勢生效,開始我想著如果能再UIScrollView的手勢代理方法中區(qū)分出Flutter CustomScrollView的手勢,然后再做調(diào)整。但是這條路是錯誤的,因為Flutter的滑動手勢并不是采用系統(tǒng)提供的手勢,在斷點時發(fā)現(xiàn),觸發(fā)之后,并沒有相關(guān)flutter 上的 系統(tǒng)手勢類。所以我改變了思路用以下去解決這個問題:
- 如果當(dāng)前觸摸在FlutterView上,我們的子類UIScrollView 手勢就不接收touch事件,控制其手勢不會生效;
- 繼承FLBFlutterViewContainer父類,重寫touches相關(guān)方法,手動調(diào)整UIScrollView 的contentOffset(注意: 我們混合棧使用的是閑魚的FlutterBoost, FLBFlutterViewContainer繼承自FlutterViewController)
上代碼:
-
自定義繼承自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
-
自定義一個繼承自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
處理之后的效果:

總結(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)于做了個手勢的方向的識別。