I use a ClipRect to only show a part of a widget, but I need it to shrink to its content.
Is there any way to do that?
class ClipExample extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: ClipRect(
clipper: MyClipper(),
child: Image.asset('test.jpg'),
),
);
}
}
class MyClipper extends CustomClipper<Rect>{
#override
Rect getClip(Size size) {
return Rect.fromLTWH(100, 250, 150, 100);
}
#override
bool shouldReclip(CustomClipper<Rect> oldClipper) => false;
}
Related
Basically, I want to render a ModalRoute that is dependent on some widget in the route below it.
To achieve that I am using a GlobalKey which I attach to a widget in the lower route:
/// in LOWER route (widget that is in lower route)
#override
Widget build(BuildContext context) {
return Container(
key: globalKey,
child: ..,
);
}
/// UPPER route (different class!)
/// called using a function on tap in the lower route widget
/// `showModalRoute(globalKey)`
#override
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
final renderBox = globalKey.currentContext.findRenderObject() as RenderBox;
final Size size = renderBox.size;
return SizedBox(
width: size.width,
height: size.height,
child: ..,
);
}
I am trying to make this respond to orientation changes. The widget in the lower route changes size when the orientation changes.
The problem here is that the upper route seems to be built before the lower route. Maybe this is not the case, however, the size is always the previous size, i.e. I get the landscape size when rotating to potrait and vise versa as if the upper route was built before the lower route (my assumption). The same applies to the position. I basically get the previous RenderBox.
Is there any way for me to get the current position of my widget, i.e. via renderBox.localToGlobal(0, 0)? I imagine that I could achieve this by having the buildPage render after the GlobalKey has the new size.
Check this code tell me if it worked as you expected
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
void main() => runApp(MyApp());
StreamController<MyWidgetStatus> firstRouteStatus =
StreamController.broadcast();
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Orination Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with WidgetsBindingObserver {
GlobalKey _stateKey;
MyWidgetStatus _status;
double height;
double width;
#override
void initState() {
WidgetsBinding.instance.addObserver(this);
_stateKey = GlobalKey();
SchedulerBinding.instance.addPostFrameCallback(_calculatePositionOffset);
super.initState();
}
_calculatePositionOffset(_) {
_status = _getPositions(_stateKey);
firstRouteStatus.add(_status);
print("position = ${_status.position}");
}
MyWidgetStatus _getPositions(_key) {
final RenderBox renderBoxRed = _key.currentContext.findRenderObject();
final position = renderBoxRed.localToGlobal(Offset.zero);
final height = renderBoxRed.constraints.maxHeight;
final width = renderBoxRed.constraints.maxWidth;
return MyWidgetStatus(position: position, width: width, hight: height);
}
void didChangeMetrics() {
print("Metrics changed");
SchedulerBinding.instance.addPostFrameCallback(_calculatePositionOffset);
super.didChangeMetrics();
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(
child: Icon(Icons.navigate_next),
onPressed: () {
_settingModalBottomSheet(context);
}),
body: OrientationBuilder(
builder: (context, orientation) {
return Center(
child: LayoutBuilder(
builder: (context, constraints) => SizedBox(
key: _stateKey,
height: orientation == Orientation.portrait ? 100.0 : 50,
width: orientation == Orientation.portrait ? 50.0 : 100.0,
child: Container(
color: Colors.red,
),
),
),
);
},
),
);
}
void _settingModalBottomSheet(context) {
showModalBottomSheet(
context: context,
builder: (BuildContext bc) {
return Scaffold(
body: StreamBuilder(
stream: firstRouteStatus.stream,
builder: (context, AsyncSnapshot<MyWidgetStatus> snapshot) =>
snapshot.hasData
? Container(
child: Text("Position = ${snapshot.data.position}"),
)
: Text("No Data"),
),
);
});
}
}
class MyWidgetStatus {
final Offset position;
final double hight;
final double width;
MyWidgetStatus({
this.position,
this.hight,
this.width,
});
}
Edit: if you need the information to be rendered at the beginning you can use a BehaviorSubject instead of the native StreamController like
import 'package:rxdart/rxdart.dart';
StreamController<MyWidgetStatus> firstRouteStatus =
BehaviorSubject();
you also have to add the RxDart package in pubspec.yaml it is 0.22.0 at the time of writing this line.
rxdart: ^0.22.0
When the bottom child is tapped (the height changes when its tapped), the parent color (blue) is shown while animating.
Is there anyway to prevent the animation?
Should I be thinking about this in a different way?
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Container(color: Colors.blue, child: CustomScrollView(slivers: <Widget>[
SliverList(
delegate: SliverChildListDelegate([
TapBox(color: Colors.red),
TapBox(color: Colors.white),
TapBox(color: Colors.red),
]),
)
],))
);
}
}
class TapBox extends StatefulWidget {
final Color color;
TapBox({ this.color });
#override
State<StatefulWidget> createState() => _TapBoxState();
}
class _TapBoxState extends State<TapBox> {
double height = 500;
onTap() {
setState(() {
if (height == 500) {
height = 250;
} else {
height = 500;
}
});
}
#override
Widget build(BuildContext context) {
return Material(child: InkWell(onTap: onTap, child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
return Container(width: constraints.maxWidth, height: height, color: widget.color);
})));
}
}
In iOS, I wrote a somewhat complex custom UIViewController that handles transitioning between unique child controllers; most notably, a special header view at the top of each one. I'm still trying to really wrap my head around end to end architecture in Flutter, and would like some suggestions on how to accomplish this. There are two types of headers - Arc and Profile, and each one goes from an expanded to a collapsed state as the user scrolls. Additionally, navigation between any combination of type and state can have a transition defined.
Here is how it looks when used in a TabBar for example. Transitions are handled gracefully wether nested in Tab/NavigationControllers or not.
This is what I threw together, I hope it helps (click for video):
Note:
It would be better to reduce the amount of animation controllers, ideally to a single controller that controls both the header extent and the arc curvature
There is no animation for the content below the header, but I'm sure you could add that as well.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Anim playground',
theme: ThemeData(
brightness: Brightness.dark,
),
home: AnimatedPageTest(),
);
}
}
class AnimatedPageTest extends StatefulWidget {
#override
_AnimatedPageTestState createState() => _AnimatedPageTestState();
}
class _AnimatedPageTestState extends State<AnimatedPageTest> {
bool _arc = true;
#override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(child: AnimatedPage(
appearance: _arc ? HeaderAppearance.arc : HeaderAppearance.profile,
backgroundImage: _arc ? 'assets/earth.jpg' : 'assets/moon.jpg',
children: List.generate(30, (index) => ListTile(title: Text('index'),)),
),),
persistentFooterButtons: <Widget>[
FlatButton(
child: Text('Switch'),
onPressed: () {
setState(() {
_arc = !_arc;
});
},
)
],
);
}
}
enum HeaderAppearance { arc, profile }
double _getTargetMaxExtent(HeaderAppearance appearance) {
if (appearance == HeaderAppearance.arc) {
return 150.0;
} else {
return 75.0;
}
}
double _getTargetArcAnimationValue(HeaderAppearance appearance) {
if (appearance == HeaderAppearance.arc) {
return 1.0;
} else {
return 0.0;
}
}
class AnimatedPage extends StatefulWidget {
AnimatedPage({Key key, this.appearance, this.backgroundImage, this.children}) : super(key: key);
final HeaderAppearance appearance;
final String backgroundImage;
final List<Widget> children;
#override
_AnimatedPageState createState() => _AnimatedPageState();
}
class _AnimatedPageState extends State<AnimatedPage> with SingleTickerProviderStateMixin {
AnimationController _maxExtentAnimation;
#override
void initState() {
super.initState();
_maxExtentAnimation = AnimationController.unbounded(vsync: this, value: _getTargetMaxExtent(widget.appearance));
}
#override
void didUpdateWidget(AnimatedPage oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.appearance != oldWidget.appearance) {
_maxExtentAnimation.animateTo(
_getTargetMaxExtent(widget.appearance),
duration: Duration(milliseconds: 600),
curve: Curves.easeInOut,
);
}
}
#override
void dispose() {
_maxExtentAnimation.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _maxExtentAnimation,
builder: (context, child) {
return CustomScrollView(
slivers: <Widget>[
SliverPersistentHeader(
pinned: true,
delegate: AnimatedHeaderDelegate(
appearance: widget.appearance,
backgroundImage: widget.backgroundImage,
minExtent: 50.0,
maxExtent: _maxExtentAnimation.value,
),
),
child,
],
);
},
child: SliverList(delegate: SliverChildListDelegate(widget.children)),
);
}
}
class AnimatedHeaderDelegate extends SliverPersistentHeaderDelegate {
AnimatedHeaderDelegate({this.appearance, this.backgroundImage, this.minExtent, this.maxExtent});
final HeaderAppearance appearance;
final String backgroundImage;
#override
final double minExtent;
#override
final double maxExtent;
#override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
final shrinkRelative = shrinkOffset / (maxExtent - minExtent);
return AnimatedHeader(
appearance: appearance,
backgroundImage: backgroundImage,
curvatureMultiplier: 1.0 - shrinkRelative,
);
}
#override
bool shouldRebuild(AnimatedHeaderDelegate oldDelegate) {
return appearance != oldDelegate.appearance ||
minExtent != oldDelegate.minExtent ||
maxExtent != oldDelegate.maxExtent;
}
}
class AnimatedHeader extends StatefulWidget {
AnimatedHeader({Key key, this.appearance, this.backgroundImage, this.curvatureMultiplier}) : super(key: key);
final HeaderAppearance appearance;
final String backgroundImage;
final double curvatureMultiplier;
#override
_AnimatedHeaderState createState() => _AnimatedHeaderState();
}
class _AnimatedHeaderState extends State<AnimatedHeader> with TickerProviderStateMixin {
AnimationController _arcAnimation;
#override
void initState() {
super.initState();
_arcAnimation = AnimationController(
vsync: this,
value: _getTargetArcAnimationValue(widget.appearance),
duration: Duration(milliseconds: 600),
);
}
#override
void didUpdateWidget(AnimatedHeader oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.appearance != oldWidget.appearance) {
_arcAnimation.animateTo(_getTargetArcAnimationValue(widget.appearance));
}
}
#override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: CurvedAnimation(parent: _arcAnimation, curve: Curves.linear),
builder: (context, child) {
return ClipPath(
clipper: ArcClipper(
curvature: _arcAnimation.value * widget.curvatureMultiplier,
),
clipBehavior: Clip.antiAlias,
child: child,
);
},
child: Stack(
fit: StackFit.expand,
children: <Widget>[
AnimatedSwitcher(
duration: Duration(milliseconds: 600),
child: Container(
key: ValueKey(widget.backgroundImage),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(widget.backgroundImage),
fit: BoxFit.cover,
),
),
),
),
Center(
child: Text(
'TITLE',
style: TextStyle(fontSize: 30.0, fontWeight: FontWeight.w500),
),
),
],
),
);
}
}
class ArcClipper extends CustomClipper<Path> {
ArcClipper({this.curvature});
final double curvature;
#override
Path getClip(Size size) {
if (curvature == 0.0) {
return Path()..addRect(Offset.zero & size);
} else {
return Path()
..moveTo(0.0, 0.0)
..lineTo(size.width, 0.0)
..lineTo(size.width, size.height)
..quadraticBezierTo(size.width / 2, size.height - size.height * 0.4 * curvature, 0.0, size.height)
..close();
}
}
#override
bool shouldReclip(ArcClipper oldClipper) {
return curvature != oldClipper.curvature;
}
}
I have made a grid of buttons using flutter but now I want to swipe through 2 or more buttons in a single drag such that all the buttons through which I am dragging gets selected.
I have checked out some questions on the same and I was redirected to use gesture detector but that's not enough. I need certain properties or better a sample code such that I am able to work through it.
an example of the dragable app is http://a5.mzstatic.com/us/r30/Purple60/v4/6f/00/35/6f0035d3-1bab-fcbb-cb13-8ab46cf3c44d/screen696x696.jpeg
You can manually hit test RenderBox and extract a specific RenderObject of your choice.
We could for example add the following renderobject above our buttons:
class Foo extends SingleChildRenderObjectWidget {
final int index;
Foo({Widget child, this.index, Key key}) : super(child: child, key: key);
#override
RenderObject createRenderObject(BuildContext context) {
return _Foo()..index = index;
}
#override
void updateRenderObject(BuildContext context, _Foo renderObject) {
renderObject..index = index;
}
}
class _Foo extends RenderProxyBox {
int index;
}
Then use a Listener to extract all _Foo found under the pointer.
Here's a full application using this principle:
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: Grid(),
);
}
}
class Grid extends StatefulWidget {
#override
GridState createState() {
return new GridState();
}
}
class GridState extends State<Grid> {
final Set<int> selectedIndexes = Set<int>();
final key = GlobalKey();
final Set<_Foo> _trackTaped = Set<_Foo>();
_detectTapedItem(PointerEvent event) {
final RenderBox box = key.currentContext.findRenderObject();
final result = BoxHitTestResult();
Offset local = box.globalToLocal(event.position);
if (box.hitTest(result, position: local)) {
for (final hit in result.path) {
/// temporary variable so that the [is] allows access of [index]
final target = hit.target;
if (target is _Foo && !_trackTaped.contains(target)) {
_trackTaped.add(target);
_selectIndex(target.index);
}
}
}
}
_selectIndex(int index) {
setState(() {
selectedIndexes.add(index);
});
}
#override
Widget build(BuildContext context) {
return Listener(
onPointerDown: _detectTapedItem,
onPointerMove: _detectTapedItem,
onPointerUp: _clearSelection,
child: GridView.builder(
key: key,
itemCount: 6,
physics: NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1.0,
crossAxisSpacing: 5.0,
mainAxisSpacing: 5.0,
),
itemBuilder: (context, index) {
return Foo(
index: index,
child: Container(
color: selectedIndexes.contains(index) ? Colors.red : Colors.blue,
),
);
},
),
);
}
void _clearSelection(PointerUpEvent event) {
_trackTaped.clear();
setState(() {
selectedIndexes.clear();
});
}
}
class Foo extends SingleChildRenderObjectWidget {
final int index;
Foo({Widget child, this.index, Key key}) : super(child: child, key: key);
#override
_Foo createRenderObject(BuildContext context) {
return _Foo()..index = index;
}
#override
void updateRenderObject(BuildContext context, _Foo renderObject) {
renderObject..index = index;
}
}
class _Foo extends RenderProxyBox {
int index;
}
I don't like this code at all, but it seems to be working
import 'package:flutter/material.dart';
class TestScaffold extends StatefulWidget {
#override
State<StatefulWidget> createState() => _TestScaffoldState();
}
List<_SquareButton> _selectedList = [];
class _TestScaffoldState extends State<TestScaffold> {
List<_SquareButton> buttons = [
_SquareButton('1'),
_SquareButton('2'),
_SquareButton('3'),
_SquareButton('4'),
_SquareButton('5'),
_SquareButton('6'),
_SquareButton('7'),
_SquareButton('8'),
_SquareButton('9'),
_SquareButton('10'),
_SquareButton('11'),
_SquareButton('12'),
_SquareButton('13'),
_SquareButton('14'),
_SquareButton('15'),
_SquareButton('16'),
];
Map<Rect, _SquareButton> positions = {};
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Test'),),
body: GestureDetector(
onPanDown: (details) {
checkGesture(details.globalPosition);
},
onPanUpdate: (details) {
checkGesture(details.globalPosition);
},
child: GridView.count(crossAxisCount: 4,
physics: NeverScrollableScrollPhysics(),
children: buttons,),)
);
}
initPositions() {
if (positions.isNotEmpty) return;
buttons.forEach((btn) {
RenderBox box = btn.bKey.currentContext.findRenderObject();
Offset start = box.localToGlobal(Offset.zero);
Rect rect = Rect.fromLTWH(start.dx, start.dy, box.size.width, box.size.height);
positions.addAll({rect: btn});
});
}
checkGesture(Offset position) {
initPositions();
positions.forEach((rect, btn) {
if (rect.contains(position)) {
if (!_selectedList.contains(btn)) {
_selectedList.add(btn);
btn.state.setState((){});
}
}
});
}
}
class _SquareButton extends StatefulWidget {
_SquareButton(this.title);
final String title;
final GlobalKey bKey = GlobalKey();
State state;
#override
State<StatefulWidget> createState() {
state = _SquareButtonState();
return state;
}
}
class _SquareButtonState extends State<_SquareButton> {
#override
Widget build(BuildContext context) {
return Padding(key: widget.bKey, padding: EdgeInsets.all(4.0), child: Container(
color: _selectedList.contains(widget) ? Colors.tealAccent : Colors.teal,
child: Text(widget.title),
alignment: Alignment.center,
),);
}
}
There is a moment.
If you enable scrolling - GestureDetector not always work on vertical movements
I want show a popup at touch Coordinates. I am using Stack and Positioned widgets to place the popup.
You can add a GestureDetector as parent of stack, and register onTapDownDetails listener. This should call your listener on every tapdown event, with global offset of the tap in TapDownDetails parameter of the your listener.
Here is sample code demonstrating the same.
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: new MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
#override
MyHomePageState createState() => new MyHomePageState();
}
class MyHomePageState extends State<MyHomePage> {
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Popup Demo'),
),
body: new MyWidget());
}
}
class MyWidget extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return new MyWidgetState();
}
}
class MyWidgetState extends State<MyWidget> {
double posx = 100.0;
double posy = 100.0;
void onTapDown(BuildContext context, TapDownDetails details) {
print('${details.globalPosition}');
final RenderBox box = context.findRenderObject();
final Offset localOffset = box.globalToLocal(details.globalPosition);
setState(() {
posx = localOffset.dx;
posy = localOffset.dy;
});
}
#override
Widget build(BuildContext context) {
return new GestureDetector(
onTapDown: (TapDownDetails details) => onTapDown(context, details),
child: new Stack(fit: StackFit.expand, children: <Widget>[
// Hack to expand stack to fill all the space. There must be a better
// way to do it.
new Container(color: Colors.white),
new Positioned(
child: new Text('hello'),
left: posx,
top: posy,
)
]),
);
}
}
You can simply use a Listener as the parent of your Stack and listen to it's onPointerDown event like so:
Listener(
onPointerDown: (event) {
// use event.localPosition.dx or event.localPosition.dy
},
child: Stack(
children: [
],
),
)