寫這個的一切原由都得從我某天切換了酷安App的夜間模式提及,看個Gif,忽略圖中其餘無關項。 bash
起初用showOverlay的方式來作一個,效果始終很差,有違和感。後來果斷採用自定義路由,最後整個動畫的難點全在自定義路由上,若是你對原理不感興趣,滑動到最後有現成的代碼ide
新的頁面是由點擊的控件中心所在的座標位置呈一個圓形逐漸擴散開來,最後撐滿整個屏幕,不會啥繪圖工具,用我Mac自帶的繪圖頂一下 工具
final RenderBox renderBox = iconContext.findRenderObject();
offset = renderBox.localToGlobal(renderBox.size.center(Offset.zero));
複製代碼
也就是說,圓的最大會恰好覆蓋住手機屏幕最遠的那個角落,若是小於了,則第二個頁面顯示不完整,大於了則會浪費多餘的動畫時長,看一下路由圓的直徑計算代碼,用了比較笨的判斷,用簡單的勾股定理計算出控件中心的座標到屏幕最遠的距離,路由頁面的大小即爲該距離的二倍。佈局
final RenderBox renderBox = context.findRenderObject();
offset = renderBox.localToGlobal(renderBox.size.center(Offset.zero));
if (offset.dx > MediaQuery.of(context).size.width / 2) {
if (offset.dy > MediaQuery.of(context).size.height / 2) {
circleRadius = sqrt(pow(offset.dx, 2) + pow(offset.dy, 2)).toDouble();
} else {
circleRadius = sqrt(pow(offset.dx, 2) +
pow(MediaQuery.of(context).size.height - offset.dy, 2))
.toDouble();
}
}
if (offset.dx <= MediaQuery.of(context).size.width / 2) {
if (offset.dy > MediaQuery.of(context).size.height / 2) {
circleRadius = sqrt(
pow(MediaQuery.of(context).size.width - offset.dx, 2) +
pow(offset.dy, 2))
.toDouble();
} else {
circleRadius = sqrt(
pow(MediaQuery.of(context).size.width - offset.dx, 2) +
pow(MediaQuery.of(context).size.height - offset.dy, 2))
.toDouble();
}
}
}
複製代碼
首先咱們實現這個圓的動畫,我就不單獨寫demo了,直接拿個人工具箱作實驗,定位到PageRouteBuilder的關鍵代碼動畫
transitionsBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> _,
Widget child,
) {
return Stack(
alignment: Alignment.center,
children: <Widget>[
SizedBox(
height: routeConfig.circleRadius * 2 * animation.value,
width: routeConfig.circleRadius * 2 * animation.value,
child: ClipOval(
child: Align(
alignment: Alignment.center,
child: Container(
color: Colors.red,
),
),
),
),
],
);
}
複製代碼
這部分的代碼比較好理解,routeConfig.circleRadius即爲整個圓最大時的半徑,路由的新頁面收SziedBox的限制,而SizedBox包裹ClipOval控件來實現圓形,這部分的效果以下 ui
return Stack(
alignment: Alignment.center,
children: <Widget>[
Positioned(
top: routeConfig.offset.dy -
routeConfig.circleRadius * animation.value,
left: routeConfig.offset.dx -
routeConfig.circleRadius * animation.value,
child: SizedBox(
height: routeConfig.circleRadius * 2 * animation.value,
width: routeConfig.circleRadius * 2 * animation.value,
child: ClipOval(
child: Align(
alignment: Alignment.center,
child: Container(
color: Colors.red,
),
),
),
),
),
],
);
複製代碼
傳入的是負數是由於初始距離上、左屏幕的距離爲0,若是是正數,圓只會愈來愈遠離屏幕上與左,看一下此時的效果 this
return Stack(
alignment: Alignment.center,
children: <Widget>[
Positioned(
top: routeConfig.offset.dy -
routeConfig.circleRadius * animation.value,
left: routeConfig.offset.dx -
routeConfig.circleRadius * animation.value,
child: SizedBox(
height: routeConfig.circleRadius * 2 * animation.value,
width: routeConfig.circleRadius * 2 * animation.value,
child: ClipOval(
child: Stack(
children: <Widget>[
Positioned(
top: routeConfig.circleRadius * animation.value -
routeConfig.offset.dy,
left: routeConfig.circleRadius * animation.value -
routeConfig.offset.dx,
child: Align(
alignment: Alignment.center,
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: child,
),
),
)
],
),
),
),
),
],
);
複製代碼
效果以下: spa
import 'dart:math';
import 'package:flutter/material.dart';
class RouteConfig {
Offset offset;
double circleRadius;
RouteConfig.fromContext(BuildContext context) {
final RenderBox renderBox = context.findRenderObject();
offset = renderBox.localToGlobal(renderBox.size.center(Offset.zero));
if (offset.dx > MediaQuery.of(context).size.width / 2) {
if (offset.dy > MediaQuery.of(context).size.height / 2) {
circleRadius = sqrt(pow(offset.dx, 2) + pow(offset.dy, 2)).toDouble();
} else {
circleRadius = sqrt(pow(offset.dx, 2) +
pow(MediaQuery.of(context).size.height - offset.dy, 2))
.toDouble();
}
}
if (offset.dx <= MediaQuery.of(context).size.width / 2) {
if (offset.dy > MediaQuery.of(context).size.height / 2) {
circleRadius = sqrt(
pow(MediaQuery.of(context).size.width - offset.dx, 2) +
pow(offset.dy, 2))
.toDouble();
} else {
circleRadius = sqrt(
pow(MediaQuery.of(context).size.width - offset.dx, 2) +
pow(MediaQuery.of(context).size.height - offset.dy, 2))
.toDouble();
}
}
}
}
// double circleRadius
class RippleRoute extends PageRouteBuilder {
final Widget widget;
final RouteConfig routeConfig;
RippleRoute(this.widget, this.routeConfig)
: super(
// 設置過分時間
transitionDuration: Duration(seconds: 1),
// 構造器
pageBuilder: (
// 上下文和動畫
BuildContext context,
Animation<double> animation,
Animation<double> _,
) {
return widget;
},
opaque: false,
transitionsBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> _,
Widget child,
) {
return Stack(
alignment: Alignment.center,
children: <Widget>[
Positioned(
top: routeConfig.offset.dy -
routeConfig.circleRadius * animation.value,
left: routeConfig.offset.dx -
routeConfig.circleRadius * animation.value,
child: SizedBox(
height: routeConfig.circleRadius * 2 * animation.value,
width: routeConfig.circleRadius * 2 * animation.value,
child: ClipOval(
child: Stack(
children: <Widget>[
Positioned(
top: routeConfig.circleRadius * animation.value -
routeConfig.offset.dy,
left: routeConfig.circleRadius * animation.value -
routeConfig.offset.dx,
child: Align(
alignment: Alignment.center,
child: Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: child,
),
),
)
],
),
),
),
),
],
);
},
);
}
複製代碼
###使用方法: 在Flutter裏面,可將點擊的按鈕套上一個Builder,如如下的樣式,在跳轉邏輯中以下,各類計算都被我封裝到了RouteConfig這個類裏面了,經過context構造並傳入PageRouteBuilder就好了3d
Builder(
builder: (iconContext) {
return InkWell(
child: Icon(
Icons.***,
),
onTap: () {
Navigator.of(context).push(
RippleRoute(
NewPage(),
RouteConfig.fromContext(context)),
);
},
);
},
);
複製代碼
不只是路由相同的頁面來切換主題,也能夠用於任何的路由場景 code
那如何用這個路由切換主題?將你不包含Theme的頁面獨立出來,再切換主題路由新頁面是套上一個新的Theme就好啦 總結了下,是寫騷了一點,不過個人確想不到比較官方的寫法了哈哈,好幾迴轉gif我就懶得貼表情包了哈哈