處理和響應(yīng)觸摸效果
我們可以用GestureDetector實(shí)現(xiàn)這個效果
GestureDetector(
///手勢觸摸移動開始,這里我們可以記錄開始的觸摸點(diǎn),用來判斷移動比例和動畫的初始點(diǎn)
onHorizontalDragStart: onStart,
///手勢觸摸移動中,這里生成tab的切換效果,具體效果可以用戶自定義,效果代碼都在delegate類里.
onHorizontalDragUpdate: onUpdate,
///手勢觸摸結(jié)束,這里判斷是切換到下一張卡片還是滑動失敗,回滾當(dāng)前tab
onHorizontalDragEnd: onEnd,
child: child,
);
觸摸開始
///記錄觸摸初始點(diǎn)
onStart(DragStartDetails details) {
dragStart = details.globalPosition;
...
}
觸摸移動中
onUpdate(DragUpdateDetails details) {
if (dragStart != null) {
///滑動方向,向左或向右
SlideDirection slideDirection;
///滑動進(jìn)度.[0, 1]
double slidePercent = 0.0;
///當(dāng)前觸摸的點(diǎn)
final newPosition = details.globalPosition;
///拖動距離,如果大于零是向右拖動,如果小于零是向左拖動.
///當(dāng)前點(diǎn)的x軸位置減去觸摸起始點(diǎn)的x軸位置
final dx = newPosition.dx - dragStart.dx;
slidePercent = (dx / FULL_TRANSITION_PX).abs().clamp(0.0, 1.0).toDouble();
if (dx > 0) {
slideDirection = SlideDirection.leftToRight;
} else if (dx < 0) {
slideDirection = SlideDirection.rightToLeft;
} else {
slideDirection = SlideDirection.none;
slidePercent = 0;
}
...
}
}
動畫處理
我們在觸摸手勢結(jié)束后開始動畫處理,動畫分為兩個,一個是滑動成功的動畫切換到下一個tab,一個是滑動失敗(比如滑動距離很小,不需要跳轉(zhuǎn)到下一個頁面).這里的value是觸摸手勢的滑動比例和Animation的value,它們兩個的值是相同的,這樣可以有連貫的動畫效果.
onAnimatedStart({SlideUpdate slideUpdate}) {
Duration duration;
///判斷是否成功, 滑動的值 是否大于我們設(shè)置的滑動成功的比例,我們這里設(shè)置的是0.5.
_isSlideSuccess = value >= slideSuccessProportion;
///成功
if (_isSlideSuccess) {
final slideRemaining = 1.0 - value;
///計(jì)算tab切換的時間
duration = Duration(
milliseconds: (slideRemaining / PERCENT_PER_MILLISECOND).round());
_animationController.duration = duration;
///動畫向前運(yùn)行到1,動畫結(jié)束后切換當(dāng)前tab為下一頁tab
_animationController.forward(from: value).whenComplete(() =>
animationCompleted());
} else {
///失敗,回退當(dāng)當(dāng)前tab
duration =
Duration(milliseconds: (value / PERCENT_PER_MILLISECOND).round());
_animationController.duration = duration;
///將動畫值回退到0.
_animationController.reverse(from: value);
}
}
效果自定義
這里用了AnyTabDelegate抽象類,我們可以繼承這個抽象類來實(shí)現(xiàn)任意效果.這樣做最大的好處就是分離ui和邏輯的處理.
abstract class AnyTabDelegate {
///tab列表
List<Widget> tabs;
AnyTabDelegate({@required this.tabs});
int get length => tabs.length;
///邏輯處理后調(diào)用的build
Widget build(
BuildContext context,
///當(dāng)前tab頁
int activeIndex,
///下一頁
int nextPageIndex,
///動畫值,它的value就是手勢觸摸的值和動畫執(zhí)行的值.
Animation animation,
///觸摸的初始點(diǎn),用于動畫的初始點(diǎn)
Offset startingOffset,
);
}
這里我們來看一下CircularAnyTabDelegate的實(shí)現(xiàn),這里我們用了ClipOval來剪裁下一頁要顯示的tab,如果傳入的percentage是0則完全不顯示,是1這完全顯示.
class CircularAnyTabDelegate extends AnyTabDelegate {
CircularAnyTabDelegate({@required List<Widget> tabs})
: assert(tabs != null && tabs.length > 0),
super(tabs: tabs);
@override
Widget build(BuildContext context, int activeIndex, int nextPageIndex,
Animation animation, Offset startingOffset) {
return Stack(
children: [
tabs[activeIndex],
ClipOval(
clipper: CircularClipper(
percentage: animation.value,
offset: startingOffset,
),
child: tabs[nextPageIndex],
)
],
);
}
}
再往下看一下CircularClipper的代碼.
class CircularClipper extends CustomClipper<Rect> {
///百分比, 0-> 1,1 => 全部顯示
final double percentage;
///初始點(diǎn)
final Offset offset;
const CircularClipper({this.percentage = 0, this.offset = Offset.zero});
@override
Rect getClip(Size size) {
///計(jì)算觸摸初始點(diǎn)到邊緣四個角的最大距離,也就是我們剪裁圓的半徑
double maxValue = maxLength(size, offset) * percentage;
return Rect.fromLTRB(-maxValue + offset.dx, -maxValue + offset.dy, maxValue + offset.dx, maxValue + offset.dy);
}
@override
bool shouldReclip(CircularClipper oldClipper) {
return percentage != oldClipper.percentage || offset != oldClipper.offset;
}
/// |
/// 1 | 2
/// ---------
/// 3 | 4
/// |
/// 計(jì)算矩形內(nèi)點(diǎn)到邊緣的最大距離,這里我們把矩形分成四塊,
/// 點(diǎn)在那一塊,最大的距離就是這個點(diǎn)到對角矩形最遠(yuǎn)那個點(diǎn)的距離
double maxLength(Size size, Offset offset) {
double centerX = size.width / 2;
double centerY = size.height / 2;
if (offset.dx < centerX && offset.dy < centerY) {
///1
return getEdge(size.width - offset.dx, size.height - offset.dy);
} else if (offset.dx > centerX && offset.dy < centerY) {
///2
return getEdge(offset.dx, size.height - offset.dy);
} else if (offset.dx < centerX && offset.dy > centerY) {
///3
return getEdge(size.width - offset.dx, offset.dy);
} else {
///4
return getEdge(offset.dx, offset.dy);
}
}
double getEdge(double width, double height) {
return sqrt(pow(width, 2) + pow(height, 2));
}
}