於 flutter 2019 web portfolio 中抽出的 package
- CustomPanGestureRecognizer
- PanelGestureRecognizer
- ScrollerNestablePanelGestureController
- DebugBox
- ResponsiveScreen/ResponsiveELt
- ContextKeeper
- Dim
CustomPanGestureRecognizer | source
為了自定義 [GestureRecognizer] 需要實作 [RawGestureDetector], [PanelGestureRecognizer] 透過實作 [RawGestureDetector] 來解決當一個 Scrollable 物件其 Gesture 在接受 Scroll 事件後,便無法接收 Panel 平移的事件,如當 Panel 巢狀一個 Scrollable 子物件時,其 event arena 如下
event arena
[scrollEvent] priority-1
|
|
[panEvent] priority-2
當只有 SlidingPanel 時可平移 Panel 但是,當其內部置入了一個 Scrollable 以後,原來 的 onPan 事件則因優先權低於子層而被 Scroll 事件擋掉了,為了取得其 event arena 的所有權,需要自行實作客制的 [GestureRecognizer], 而 [CustomPanGestureRecognizer] 便是為了解決平移事件被內容物事件擋掉的問題,而需實 作一個可以外部偵聽 控制的 [GestureRecognizer]
interface
/// on pan down, 其 return 值決定是否取得 pan 事件,還是讓渡
/// return false to yield the event arena
/// return true to win the event arena
final bool Function(Offset offset) onPanDown;
/// 當 pan 成功 update 時
final Function(Offset offset) onPanUpdate;
/// 當 pan 結束
final Function(Offset offset) onPanEnd;
CustomPanGestureRecognizer({
required this.onPanDown,
required this.onPanUpdate,
required this.onPanEnd
});
PanelGestureRecognizer | source
當 Panel 內置一個 Scrollable content 時,Gesture 事件因優先權在子層而無法控制 Panel,所以需要透過實作 [RawGestureDetector] 以對 [GestureRecognizer] 作進一步控制,[PanelGestureRecognizer] 的 Controller
- [ScrollerNestablePanelGestureController]
interface
class PanelGestureRecognizer extends RawGestureDetector {
PanelGestureRecognizer({
required Widget child,
required ScrollerNestablePanelGestureController gestureController
}) : super(
gestures: gestureController.detector.createGestures(),
child: child
);
}
Example
class Sample {
Widget baseBuilder(BuildContext context, Widget child) {
return LayoutBuilder(
builder: (context, constraint) =>
PanelGestureDetector(
gestureController: widget.gestureController,
child: SingleChildScrollView(
controller: widget.gestureController.pageController,
child: Column(
children: <Widget>[
SizedBox(height: widget.marginTop),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(24.0)),
boxShadow: [
BoxShadow(
blurRadius: 10.0,
color: Colors.grey,
),
]
),
child: child,
),
],
),
))
);
}
}
ScrollerNestablePanelGestureController | source
由 [PageController]/[PanelController] 二個 controller 組成,交替控制切換其所有權, 其行為定義如下
- 當 Scrollable content 於起點時,且使用者往下 pan,這時因為已經沒有額外的可視內容,判定使用者真正想要做的是想將 Panel 下拉,而將所有權交給 Panel
- 當 Scrollable content 於結尾時,且使用者往上 pan,將所有權交給 Panel
- 其餘狀態所有權為 Scrollable content
interface
class ScrollerNestablePanelGestureController {
final PageController pageController;
final PanelController panelController;
}
example
class Sample {
late ScrollerNestedPanelGestureController gestureController;
ScrollerNestableSlidingUpPanel? panel;
@override
void initState() {
// TODO: implement initState
super.initState();
gestureController = ScrollerNestedPanelGestureController(
pageController: PageController(),
panelController: PanelController(),
);
}
Widget slidingUp(Widget body) {
BorderRadiusGeometry radius = BorderRadius.only(
topLeft: Radius.circular(24.0),
topRight: Radius.circular(24.0),
);
final double cheight = 35;
final double marginTop = 20;
final double panelHeight = ScreenUtil.screenHeightDp - 20;
final double collapseW = max(ScreenUtil.screenWidthDp / 3, 210);
panel = ScrollerNestableSlidingUpPanel(
renderPanelSheet: false,
minHeight: cheight + marginTop,
maxHeight: panelHeight,
backdropEnabled: true,
backdropTapClosesPanel: true,
gestureController: gestureController,
panel: PortfolioPanel(
width: ScreenUtil.screenWidthDp,
height: panelHeight,
marginTop: marginTop + cheight,
gestureController: gestureController
),
collapsed: PortfolioCollapseArea(
width: collapseW, height: cheight, marginTop: marginTop),
body: body,
borderRadius: radius,
parallaxEnabled: true,
);
assert(panel?.controller != null);
return ConstrainedBox(
constraints: BoxConstraints(
minHeight: ScreenUtil.screenConstraintMax.minHeight,
minWidth: ScreenUtil.screenConstraintMax.minWidth,
),
child: panel,
);
}
}
DebugBox | source
於 Container 繪制一亂數色彩的 border, 用於 debug 視覺化 Container 大小
interface
class DebugBox extends StatelessWidget {
final Widget child;
final Color borderColor;
DebugBox({required this.child}): borderColor = _random;
}
turning debug off
DebugBox.setBoxDebugOff();
turning debug on
DebugBox.setBoxDebugOn();
ResponsiveScreen/ResponsiveELt | source
二者功能皆同,只是情境不同,均需使用在可取得 Constraint (如 LayoutBuilder) 下, 透過已知 Constraint 的情況下,依據輸入的大/中/小 情境下,提供相對應版本的 Widget
ResponsiveScreen - example demo
class Example{
Widget _buildLargeScreen(BuildContext context, BoxConstraints constraints) {
final lcolConstraints = BoxConstraints(
maxWidth: HomeLCol.screenWidthLD,
maxHeight: constraints.maxHeight,
);
final rcolConstraints = BoxConstraints(
maxWidth: HomeRCol.screenWidthLD,
maxHeight: constraints.maxHeight,
);
_D.d(()=>'rebuild _buildLargeScreen homeL(${HomeLCol.screenWidthLD}), homeR(${HomeRCol.screenWidthLD})');
return IntrinsicHeight(
child: BoundingBox(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
/// if we use expanded here would cause an unbounded height definition
/// on Column, which entails some render problem, hence we need to
/// use IntrinsincHeight to wrap on top of Column
Expanded(
child: Paddings.homePadding(
child: Paddings.homeBodyTop(
size: HomePage.homeBody.large,
child: Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
/// _buildLeftCol(context),
/// ----------------------------------------------------------
/// note: [Expanded] flex: 1.. here is neccsssary
/// The reason why [Expanded] here is neccessary is that,
/// the following Colum[HomeRCol] has a restrict width, so that
/// [Expanded] could get it's available space to fill the rest.
Expanded(child:HomeLCol(lcolConstraints)),
BoundingBox(child: Paddings.gallery(
child: HomeRCol(rcolConstraints),
)),
],
),
),
),
),
HomeFooter.large()
],
),
),
);
}
Widget _buildMediumScreen(BuildContext context, BoxConstraints constraints) {
_D.d(()=>'rebuild _buildMediumScreen home');
final rcolConstraints = BoxConstraints(
maxWidth: constraints.maxWidth - ScreenUtil.largeDesign.setWidth(HomePage.designPaddingLR)*2,
maxHeight: constraints.maxHeight,
);
return IntrinsicHeight(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Expanded(
child: Paddings.homePadding(
child: Paddings.homeBodyTop(
size: HomePage.homeBody.medium,
child: Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Expanded(child: HomeLCol(constraints)),
],
),
),
),
),
Paddings.homePadding(
child: HomeRCol(rcolConstraints)),
HomeFooter.large()
],
),
);
}
Widget _buildSmallScreen(BuildContext context, BoxConstraints constraints) {
_D.d(()=>'rebuild _buildSmallScreen home');
return Stack(
children: <Widget>[
IntrinsicHeight(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Expanded(
child: Paddings.homePadding(
child: Paddings.homeBodyTop(
size: HomePage.homeBody.small,
/// using [Row] here is necessary, since [Expanded] must be placed
/// directly inside [Flex] widgets (Column/Row/...)
child: Row(
mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Expanded(child: HomeLCol(constraints)),
],
)),
)
),
Paddings.gallerySmallMedium(child: HomeRCol(constraints)/*480*/),
HomeFooter.small()
],
),
),
],
);
}
@override
Widget build(BuildContext context) {
if (!visible)
return Container();
return LayoutBuilder(
key: ValueKey("HomePage"),
builder: (context, constraints) {
_D.d(()=>'rebuild home layout builder, ${constraints.maxWidth}');
return SingleChildScrollView(
child: ResponsiveScreen(
key: const ValueKey("HomeLayoutResponsive"),
largeScreen : ((_buildLargeScreen(context, constraints))),
mediumScreen: (_buildMediumScreen(context, constraints)),
smallScreen : (_buildSmallScreen(context, constraints)),
),
);
});
}
}
ResponsiveElt - example
class SmallGallery{
@override
@override
Widget build(BuildContext context) {
final size = IS_MOBILE ? SIZE_GALLERY : SIZE_DESKTOP;
final gallery = _GalleryBaseLayout();
// 當前 gallery 的 constraints
final w = constraints.maxWidth - ScreenUtil.largeDesign.setWidth(HomeRCol.imageDesignPaddingR);
final h = w / gallery.whRatio;
final media = TRWMedia.fromConstaints(constraints);
// 依據 constraints, 及設計上的 responsive size 來選擇 render large|medium|small
final responsive = ResponsiveElt(
responsiveSize: size,
media: media,
large: _GalleryLarge(constraints),
medium: _GalleryMedium(constraints),
small: _GallerySmall(constraints));
_D.d(() => 'isLarge : ${responsive.isLargeOrMedium(media)}');
_D.d(() => 'isMedium: ${responsive.isMedium(media)}');
_D.d(() => 'isSmall : ${responsive.isSmall(media)}');
return responsive;
}
}
ContextKeeper | source
///
/// Keep constraints on second build, until any screen changed.
///
/// For circumstances you want to use [IntrinsicHeightWidget] to infer
/// widget's size but want to avoid IntrinsicHeightWidget to rebuild
/// on every changes from children.
///
/// The size of its children is only unknown at first
/// build time but can be knowable at second time.
///