需求背景是仿iOS全局頁面向右滑動可以返回上一頁,若用戶處于PageView,則在滑動到首個Tab后繼續向右滑動也能返回上一頁。
普通頁面
CupertinoPageRoute
其實Flutter有提供一套仿iOS風格組件的Cupertino庫,其中CupertinoPageRoute
就是想要的左右滑動路由切換效果,在屏幕左側邊緣可以拖動頁面從而返回到上一頁。
代碼改動量也很小,在路由管理器中統一修改下路由方式。
/// 原寫法,均不支持右滑返回
return PageRouteBuilder(
settings: settings,
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return page;
},
);
/// 現寫法,iPhone&aPhone都支持右滑返回(如果是MaterialPageRoute僅iPhone支持右滑返回)
return CupertinoPageRoute(
settings: settings,
builder: (BuildContext context) {
return page;
},
);
CupertinoPageRoute失效
在自測時發現有個很普通的頁面無法右滑返回,那這要先了解下CupertinoPageRoute
不支持哪些情況,通過源碼可知,當頁面處于首屏、設置WillPopScope
不支持手勢返回、屬于全屏對話框、正在做入棧動畫、正在做出棧動畫、路由正在被用戶操作這幾類時,無法pop。
static bool _isPopGestureEnabled<T>(PageRoute<T> route) {
// If there's nothing to go back to, then obviously we don't support
// the back gesture.
if (route.isFirst)
return false;
// If the route wouldn't actually pop if we popped it, then the gesture
// would be really confusing (or would skip internal routes), so disallow it.
if (route.willHandlePopInternally)
return false;
// If attempts to dismiss this route might be vetoed such as in a page
// with forms, then do not allow the user to dismiss the route with a swipe.
if (route.hasScopedWillPopCallback)
return false;
// Fullscreen dialogs aren't dismissible by back swipe.
if (route.fullscreenDialog)
return false;
// If we're in an animation already, we cannot be manually swiped.
if (route.animation!.status != AnimationStatus.completed)
return false;
// If we're being popped into, we also cannot be swiped until the pop above
// it completes. This translates to our secondary animation being
// dismissed.
if (route.secondaryAnimation!.status != AnimationStatus.dismissed)
return false;
// If we're in a gesture already, we cannot start another.
if (isPopGestureInProgress(route))
return false;
// Looks like a back gesture would be welcome!
return true;
}
檢查頁面確實有設置WillPopScope
,確認是代碼遷移問題去掉即可。
含PageView頁面
當頁面中有PageView時,在PageView范圍內手勢被自己消費了,左右滑動只會切換頁面無法返回到上一頁,所以要單獨適配下,這里有兩種思路。
設置左間距
很好理解,讓PageView
非撐滿全屏的,留個邊距方便拖動頁面,和普通頁面操作手法是一樣的。
但這個解決辦法要求內容本身有邊距,如果內容是撐滿全屏情況下,又想做到產品希望的從第一個tab頁繼續右滑返回上一頁,可以通過監聽PageView
的滑動狀態來實現。
監聽到達邊界后pop
這里使用NotificationListener
監聽PageView
一旦滑動到邊界就主動pop頁面,同時還要修改它的physics
為ClampingScrollPhysics
因為iOS默認不會回調OverscrollNotification
。相關知識可以看這篇Flutter小白初探事件處理文章里有非常相似的例子。
class SwipeLeftReturnWidget extends StatelessWidget {
final Widget child;
bool _isOverscroll = false;
SwipeLeftReturnWidget(this.child);
@override
Widget build(BuildContext context) {
return NotificationListener<ScrollNotification>(
onNotification: (notification) {
switch (notification.runtimeType) {
case ScrollUpdateNotification:
_isOverscroll = false;
break;
case ScrollEndNotification:
if (_isOverscroll) {
// 返回上一頁
Navigator.of(context).pop();
}
break;
case OverscrollNotification:
if (notification.depth == 0 &&
notification.metrics.extentBefore <= 0) {
// 處于第一個tab且繼續右滑
_isOverscroll = true;
}
break;
default:
break;
}
return false;
},
child: child,
);
}
}
總結
利用系統組件可以很快地實現從邊緣拖動頁面返回上一頁;而在多Tab頁面,通過監聽Pager實現在第一個Tab頁面內繼續右滑直接返回上一頁。這兩種操作行為從觸發時機、觸發范圍、頁面是否跟手這幾個方面都有所區別,存在于同個應用可能會讓用戶感到奇怪,所以最好是能做到“在整個頁面內右滑后頁面都可以跟手返回”,目前還沒有實現到這一步,后續可以繼續深入下CupertinoPageRoute
看看如何自定義實現。