Is there a way to exclude a BottomAppBar from animations in Flutter? - animation

I tried to wrap it in a Hero widget, as that should achieve what I want. This works with BottomNavigationBar, but not with BottomAppBar, which gives this error: Scaffold.geometryOf() called with a context that does not contain a Scaffold. I tried to give it a context by using Builder, but that did not work either. Here is a sample app to showcase the behaviour:
void main() {
runApp(
MaterialApp(
home: PageOne(),
),
);
}
Widget _bottomNavigationBar() {
return BottomNavigationBar(items: [
BottomNavigationBarItem(icon: Icon(Icons.menu), title: Text('menu')),
BottomNavigationBarItem(icon: Icon(Icons.arrow_back), title: Text('back')),
]);
}
Widget _bottomAppBar() {
return BottomAppBar(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
IconButton(icon: Icon(Icons.menu), onPressed: null),
IconButton(icon: Icon(Icons.arrow_back), onPressed: null),
],
),
);
}
class PageOne extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: Hero(
tag: 'bottomNavigationBar',
child: _bottomAppBar(),
),
body: Center(
child: IconButton(
iconSize: 200,
icon: Icon(Icons.looks_two),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (context) => PageTwo()),
),
),
),
);
}
}
class PageTwo extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: Hero(
tag: 'bottomNavigationBar',
child: _bottomAppBar(),
),
body: Center(
child: IconButton(
iconSize: 200,
icon: Icon(Icons.looks_one),
onPressed: () => Navigator.pop(context),
),
),
);
}
}

The problem seems to be the animation that is used with the Navigation stack. Therefore, getting rid of the animation during the page load will stop this animation. I added the PageRouteBuilder to the PageOne class in your example to get rid of the Navigation stack animation. Use the code below to replace the PageOne class from your example.
class PageOne extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: _bottomAppBar(),
body: Center(
child: IconButton(
iconSize: 200,
icon: Icon(Icons.looks_two),
onPressed: () => Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, anim1, anim2) => PageTwo(),
transitionsBuilder: (context, anim1, anim2, child) =>
Container(child: child),
),
),
),
),
);
}
}
There are additional ways to control the animation for Navigation here
(Oh, and I got rid of the Hero() widget)

I have solved this by wrapping the Row with a Hero widget in BottomAppBar. This still allows page transitions, and does not animate the BottomAppBar as intended.
BottomAppBar(
child: Hero(
tag: 'bottomAppBar',
child: Material(
child: Row(
...
),
),
),
);
However, this has laggy animations when using a CircularNotchedRectangle.

Related

How to remove these lines between Row children

I'm new to flutter, and I'm trying to divide the screen into three parts, here's my attempt:
class Homepage extends Statelesswidget {
const HomePage({Key? key}): super (key: key);
#override
Widget build(BuildContext context) {
return Row(children: [
const Expanded(
flex: 1,
child: Scaffold(
body: Center(child: Text("Left")),
), // Scaffold
), // Expanded
Expanded(
flex: 2,
child: Scaffold(
appBar: AppBar(),
body: const Center(child: Text("Middle")),
), // Scaffold
), // Expanded
const Expanded(
flex: 1,
child: Scaffold(
body: Center(child: Text("Right")),
), // Scaffold
) // Expanded
]);
}
}
and main.dart looks like:
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return const MaterialApp(
home: HomePage(),
debugShowCheckedModeBanner: false,
);
}
}
Here's the result:
I got these vertical lines out of nowhere, how can I get rid of them?
Welcome to Flutter 💙
These lines are the boarders of the Scaffold widget which you are using inside each Expanded widget in the row.
Now intead of looking for a way to remove these border lines, I suggest you to learn more about the Scaffold widget in Flutter
Since you are new to Flutter, you have to be aware with the goal of using Scaffold widget, which is stated in the official docs as the following:
Implements the basic material design visual layout structure.
This class provides APIs for showing drawers and bottom sheets.
So in other words, we use the Scaffold Widget as a basic canvas for the screen with some properties to implement popular layout designs, such as AppBar, BottomNavigationBar, FloatingActionButton, etc...
Then, we use it once in the screen to set the basic layout, and does not required to be used multiple times for each sub part of the screen
Then your HomePage code should be something like:
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Row(children: [
const Expanded(
flex: 1,
child: Center(child: Text("Left")),
), // Expanded
Expanded(
flex: 2,
child: Column(
children: [
AppBar(),
const Expanded(
child: Center(child: Text("Middle")),
),
],
),
),
const Expanded(
flex: 1,
child: Center(child: Text("Right")),
)
]),
);
}
}

Rebuilding navigated page with new state

I have a widget called Classroom, and in this widget has a button that navigattes you to another page called ButtonPage. It has only one button that triggers a function which affects Classroom's state. It works perfectly. But the problem is that I also want to change LightButton's color with the updated state in Classroom widget. Here is codes.
Classroom
class Classroom extends StatefulWidget {
#override
_ClassroomState createState() => _ClassroomState();
}
class _ClassroomState extends State<Classroom> {
bool isLightOn = false;
final List<String> lightColor = ["red", "green", "blue"];
String currentItem = "red";
void onButtonPress() {
setState(() {
isLightOn = isLightOn;
});
}
void selectItem(String selectedItem) {
setState(() {
currentItem = selectedItem;
});
}
Widget build(BuildContext context) {
return Container(
color: Colors.blue,
padding: EdgeInsets.all(5.0),
child: Column(
children: <Widget>[
LightBulb(
isLightOn: isLightOn,
currentItem: currentItem,
),
Container(
child: MaterialButton(
textColor: Colors.white,
color: Colors.blue,
child: Text("Go to button's page"),
onPressed: () {
Navigator.push(context,
MaterialPageRoute<void>(builder: (BuildContext context) {
return ButtonPage(
isLightOn: isLightOn,
onButtonPress: onButtonPress,
);
}));
},
),
),
LightColorSelector(
selectItem: selectItem,
currentItem: currentItem,
lightColor: lightColor,
),
],
),
);
}
}
ButtonPage
class ButtonPage extends StatelessWidget {
final bool isLightOn;
final VoidCallback onButtonPress;
const ButtonPage({this.isLightOn, this.onButtonPress});
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Button Page"),
),
body: Center(
child: Container(
child: LightButton(
isLightOn: isLightOn,
onButtonPress: onButtonPress,
),
)),
);
}
}
LightButton
class LightButton extends StatelessWidget {
final bool isLightOn;
final VoidCallback onButtonPress;
const LightButton({this.isLightOn, this.onButtonPress});
#override
Widget build(BuildContext context) {
return Container(
color: Colors.red,
padding: EdgeInsets.all(5.0),
child: MaterialButton(
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
color: Colors.blue,
textColor: Colors.white,
child:
isLightOn ?? false ? Text("Turn light off") : Text("Turn light on"),
onPressed: () {
onButtonPress();
},
),
);
}
}
When I click MaterialButton which is labeled as Go to button's page, it navigates me to ButtonPage with isLightOn and onButtonPress variables. In LightButton, when onButtonPress is triggered, it rebuild only previous page. That's why Light Button's label isn't changed. How can I make it affected?

Flutter ListView.builder - How to Jump to Certain Index Programmatically

i have a screen that build using MaterialApp, DefaultTabController, Scaffold and TabBarView.
in this screen, i have body content that retreive a list of element from sqllite using StreamBuilder. i get exact 100 elements ("finite list") to be shown using ListView.
my question, using ListView.builder, How we can jump to certain index when this screen opened ?
my main screen:
...
ScrollController controller = ScrollController();
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner : false,
home: DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
backgroundColor: Pigment.fromString(UIData.primaryColor),
elevation: 0,
centerTitle: true,
title: Text(translations.text("quran").toUpperCase()),
bottom: TabBar(
tabs: [
Text("Tab1"),
Text("Tab2"),
Text("Tab3")
],
),
leading: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Expanded(
child: InkWell(
child: SizedBox(child: Image.asset("assets/images/home.png"), height: 10, width: 1,),
onTap: () => Navigator.of(context).pop(),
)
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _scrollToIndex,
tooltip: 'Testing Index Jump',
child: Text("GO"),
),
body:
TabBarView(
children: [
Stack(
children: <Widget>[
MyDraggableScrollBar.create(
scrollController: controller,
context: context,
heightScrollThumb: 25,
child: ListView(
controller: controller,
children: <Widget>[
Padding(
padding: EdgeInsets.fromLTRB(30, 15, 30, 8),
child: Container(
alignment: Alignment.center,
height: 30,
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: TextField(
style: TextStyle(color: Colors.green),
decoration: new InputDecoration(
contentPadding: EdgeInsets.all(5),
border: InputBorder.none,
filled: true,
hintStyle: new TextStyle(color: Colors.green, fontSize: 14),
prefixIcon: Icon(FontAwesomeIcons.search,color: Colors.green,size: 17,),
hintText: translations.text("search-quran"),
fillColor: Colors.grey[300],
prefixStyle: TextStyle(color: Colors.green)
),
onChanged: (val) => quranBloc.searchSurah(val),
),
)
)
),
//surah list
streamBuilderQuranSurah(context)
],
)
) // MyDraggableScrollBar
],
),
Icon(Icons.directions_transit),
Icon(Icons.directions_bike),
],
)
)));
}
Widget streamBuilderQuranSurah(BuildContext ctx){
return StreamBuilder(
stream: quranBloc.chapterStream ,
builder: (BuildContext context, AsyncSnapshot<ChaptersModel> snapshot){
if(snapshot.hasData){
return ListView.builder(
controller: controller,
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount:(snapshot.data.chapters?.length ?? 0),
itemBuilder: (BuildContext context, int index) {
var chapter =
snapshot.data.chapters?.elementAt(index);
return chapterDataCell(chapter);
},
);
}
else{
return SurahItemShimmer();
}
},
);
}
...
class MyDraggableScrollBar.dart :
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
import 'package:flutter/material.dart';
class MyDraggableScrollBar {
static Widget create({
#required BuildContext context,
#required ScrollController scrollController,
#required double heightScrollThumb,
#required Widget child,
}) {
return DraggableScrollbar(
alwaysVisibleScrollThumb: true,
scrollbarTimeToFade: Duration(seconds: 3),
controller: scrollController,
heightScrollThumb: heightScrollThumb,
backgroundColor: Colors.green,
scrollThumbBuilder: (
Color backgroundColor,
Animation<double> thumbAnimation,
Animation<double> labelAnimation,
double height, {
Text labelText,
BoxConstraints labelConstraints,
}) {
return InkWell(
onTap: () {},
child: Container(
height: height,
width: 7,
color: backgroundColor,
),
);
},
child: child,
);
}
}
i have tried find other solutions but seems not working, for example indexed_list_view that only support infinite list
and it seems flutter still not have feature for this, see this issue
Any Idea ?
You can use https://pub.dev/packages/scrollable_positioned_list. You can pass the initial index to the widget.
ScrollablePositionedList.builder(
initialScrollIndex: 12, //you can pass the desired index here//
itemCount: 500,
itemBuilder: (context, index) => Text('Item $index'),
itemScrollController: itemScrollController,
itemPositionsListener: itemPositionsListener,
);
General Solution:
To store anything which can be represented as a number/string/list of strings, Flutter provides a powerful easy-to-use plugin which stores the values needed to be stored along with a key. So the next time you need you'll need to retrieve or even update that value all that you'll need is that key.
To get started, add the shared_preferences plugin to the pubspec.yaml file,
dependencies:
flutter:
sdk: flutter
shared_preferences: "<newest version>"
Run flutter pub get from the terminal or if your using IntelliJ just click on Packages get(You'll find it somewhere around the top-right corner of your screen while viewing the pubspec.yaml file)
Once the above command is successfully executed, import the below file in your main.dart or concerned file.
import 'package:shared_preferences/shared_preferences.dart';
Now just attach a ScrollController to your ListView.builder() widget and make sure that the final/last offset is stored along with a specific key using shared_preferences whenever the user leaves the app in any way and is set when the initState of your concerned widget is called.
In order to know to detect changes in the state of our app and to act with accordance to it, we'll be inheriting WidgetsBindingObserver to our class.
Steps to follow:
Extend the WidgetsBindingObserver class along with the State class of your StatefulWidget.
Define a async function resumeController() as a function member of the above class.
Future<void> resumeController() async{
_sharedPreferences = await SharedPreferences.getInstance().then((_sharedPreferences){
if(_sharedPreferences.getKeys().contains("scroll-offset-0")) _scrollController= ScrollController(initialScrollOffset:_sharedPreferences.getDouble("scroll-offset-0"));
else _sharedPreferences.setDouble("scroll-offset-0", 0);
setState((){});
return _sharedPreferences;
});
Declare two variables one to store and pass the scrollcontroller and the other to store and use the instance of SharedPreferences.
ScrollController _scrollController;
SharedPreferences _sharedPreferences;
Call resumeController() and pass your class to the addObserver method of the instance object in WidgetsBinding class.
resumeController();
WidgetsBinding.instance.addObserver(this);
Simply paste this code in the class definition (outside other member functions)
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_scrollController.dispose();
super.dispose();
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
if(state==AppLifecycleState.paused || state==AppLifecycleState.inactive || state==AppLifecycleState.suspending)
_sharedPreferences.setDouble("scroll-offset-0", _scrollController.offset);
super.didChangeAppLifecycleState(state);
}
Pass the ScrollController() to the concerned Scrollable.
Working Example:
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> with WidgetsBindingObserver{
//[...]
ScrollController _scrollController;
SharedPreferences _sharedPreferences;
Future<void> resumeController() async{
_sharedPreferences = await SharedPreferences.getInstance().then((_sharedPreferences){
if(_sharedPreferences.getKeys().contains("scroll-offset-0")) _scrollController= ScrollController(initialScrollOffset:_sharedPreferences.getDouble("scroll-offset-0"));
else _sharedPreferences.setDouble("scroll-offset-0", 0);
setState((){});
return _sharedPreferences;
});
}
#override
void initState() {
resumeController();
WidgetsBinding.instance.addObserver(this);
super.initState();
}
#override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_scrollController.dispose();
super.dispose();
}
#override
void didChangeAppLifecycleState(AppLifecycleState state) {
if(state==AppLifecycleState.paused || state==AppLifecycleState.inactive || state==AppLifecycleState.suspending)
_sharedPreferences.setDouble("scroll-offset-0", _scrollController.offset);
super.didChangeAppLifecycleState(state);
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: Text("Smart Scroll View"),
),
body: ListView.builder(
itemCount: 50,
controller: _scrollController,
itemBuilder: (c,i)=>
Padding(
padding: EdgeInsets.symmetric(horizontal: 24,vertical: 16),
child: Text((i+1).toString()),
),
),
),
);
}
}
Solution without knowing the size of your widgets
the Solution I found without knowing the size of your widget is displaying a reverse 'sublist' from the index to the end, then scroll to the top of your 'sublist' and reset the entire list. As it is a reverse list the item will be add at the top of the list and you will stay at your position (the index).
the problem is that you can't use a listView.builder because you will need to change the size of the list
example
class _ListViewIndexState extends State<ListViewIndex> {
ScrollController _scrollController;
List<Widget> _displayedList;
#override
void initState() {
super.initState();
_scrollController = ScrollController();
_displayedList = widget.items.sublist(0, widget.items.length - widget.index);
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
SchedulerBinding.instance.addPostFrameCallback((_) {
//here the sublist is already build
completeList();
});
}
}
completeList() {
//to go to the last item(in first position)
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
//reset the list to the full list
setState(() {
_displayedList = widget.items;
});
}
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
ListView(
controller: _scrollController,
reverse: true,
children: _displayedList,
),
]
);
}
}
The https://pub.dev/packages/indexed_list_view package could maybe help you out for this. Use something like this:
IndexedListView.builder(
controller: indexScrollController,
itemBuilder: itemBuilder
);
indexScrollController.jumpToIndex(10000);
I'll present another approach, which supports list lazy loading unlike #Shinbly 's method, and also support tiles in list to resize without recalculating the correct offset of the ListView nor saving any persistent information like "#Nephew of Stackoverflow" does.
The essential key to this approach is to utilize CustomScrollView, the CustomScrollView.center property.
Here's an example based on the example code from Flutter document (widgets.CustomScrollView.2):
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
List<int> top = [];
List<int> bottom = [0];
List<int> test = List.generate(10, (i) => -5 + i);
bool positionSwitcher = true;
#override
Widget build(BuildContext context) {
positionSwitcher = !positionSwitcher;
final jumpIndex = positionSwitcher ? 1 : 9;
Key centerKey = ValueKey('bottom-sliver-list');
return Scaffold(
appBar: AppBar(
title: const Text('Press Jump!! to jump between'),
leading: IconButton(
icon: const Icon(Icons.add),
onPressed: () {
setState(() {
top.add(-top.length - 1);
bottom.add(bottom.length);
});
},
),
),
body: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
RaisedButton(
child: Text('Jump!!'),
onPressed: () => setState(() {}),
),
Text(positionSwitcher ? 'At top' : 'At bottom'),
],
),
Expanded(
child: CustomScrollView(
center: centerKey,
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int i) {
final index = jumpIndex - 1 - i;
return Container(
alignment: Alignment.center,
color: Colors.blue[200 + test[index] % 4 * 100],
height: 100 + test[index] % 4 * 20.0,
child: Text('Item: ${test[index]}'),
);
},
childCount: jumpIndex,
),
),
SliverList(
key: centerKey,
delegate: SliverChildBuilderDelegate(
(BuildContext context, int i) {
final index = i + jumpIndex;
return Container(
alignment: Alignment.center,
color: i == 0
? Colors.red
: Colors.blue[200 + test[index] % 4 * 100],
height: 100 + test[index] % 4 * 20.0,
child: Text('Item: ${test[index]}'),
);
},
childCount: test.length - jumpIndex,
),
),
],
),
)
],
),
);
}
}
Explanation:
We use single list as data source for both SliverList
During each rebuild, we use center key to reposition the second SliverList inside ViewPort
Carefully manage the conversion from SliverList index to data source list index
Notice how the scroll view build the first SliverList by passing an index starting from bottom of this SliverList (i.e. index 0 suggests last item in the first list sliver)
Give the CustomeScrollView a proper key to decide whether to "re-position" or not
Working Example:
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:scroll_to_index/scroll_to_index.dart';
class ScrollToIndexDemo extends StatefulWidget {
const ScrollToIndexDemo({Key? key}) : super(key: key);
#override
_ScrollToIndexDemoState createState() => _ScrollToIndexDemoState();
}
class _ScrollToIndexDemoState extends State<ScrollToIndexDemo> {
late AutoScrollController controller = AutoScrollController();
var rng = Random();
ValueNotifier<int> scrollIndex = ValueNotifier(0);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: ValueListenableBuilder(
valueListenable: scrollIndex,
builder: (context, index, child) {
return Text('Scroll Demo - $index');
},
),
),
body: ListView.builder(
itemCount: 100,
controller: controller,
itemBuilder: (context, index) {
return Padding(
padding: EdgeInsets.all(8),
child: AutoScrollTag(
key: ValueKey(index),
controller: controller,
index: index,
highlightColor: Colors.black.withOpacity(0.1),
child: Container(
padding: EdgeInsets.all(10),
alignment: Alignment.center,
color: Colors.grey[300],
height: 100,
child: Text(
'index: $index',
style: TextStyle(color: Colors.black),
),
),
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
scrollIndex.value = rng.nextInt(100);
await controller.scrollToIndex(scrollIndex.value, preferPosition: AutoScrollPosition.begin);
},
tooltip: 'Increment',
child: Center(
child: Text(
'Next',
textAlign: TextAlign.center,
),
),
),
);
}
}
You can use the flutter_scrollview_observer lib to implement your desired functionality without invasivity
Create and use instance of ScrollController normally.
ScrollController scrollController = ScrollController();
ListView _buildListView() {
return ListView.builder(
controller: scrollController,
...
);
}
Create an instance of ListObserverController pass it to ListViewObserver
ListObserverController observerController = ListObserverController(controller: scrollController);
ListViewObserver(
controller: observerController,
child: _buildListView(),
...
)
Now you can scroll to the specified index position
// Jump to the specified index position without animation.
observerController.jumpTo(index: 1)
// Jump to the specified index position with animation.
observerController.animateTo(
index: 1,
duration: const Duration(milliseconds: 250),
curve: Curves.ease,
);

Animate ListView item to full screen in Flutter

I would like to have my list items perform this animation (mp4) when tapped. I tried using AnimatedCrossFade but it requires its two children to be at the same level, e.g. the detail view cross-fades with the ListView not the tapped item. In fact it seems a Hero animation is the only one that can animate across widgets.
I'm having trouble using Hero. Should it wrap the list item? Does it matter if the Widget subtree is significantly different in the Hero source/destination? Also, can Hero animations be used with LocalHistoryRoutes or staggered animations?
Edit
It's now looking like what I need to do is use an Overlay, the hard part there is that I need to add the selected item to the overlay at the same position on screen where it was tapped, then the animation part would be easy. Possibly of use here is a target/follower pattern e.g. CompositedTransformTarget
You can just use Hero widget to make that kind of animation. Here's my example:
and the source code:
import 'package:flutter/material.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
#override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new FirstPage(title: 'Color Palette'),
);
}
}
class FirstPage extends StatefulWidget {
FirstPage({Key key, this.title}) : super(key: key);
final String title;
#override
_FirstPageState createState() => new _FirstPageState();
}
class _FirstPageState extends State<FirstPage> {
final palette = [
{'#E53935': 0xFFE53935},
{'#D81B60': 0xFFD81B60},
{'#8E24AA': 0xFF8E24AA},
{'#5E35B1': 0xFF5E35B1},
{'#3949AB': 0xFF3949AB},
{'#1E88E5': 0xFF1E88E5},
{'#039BE5': 0xFF039BE5},
{'#00ACC1': 0xFF00ACC1},
{'#00897B': 0xFF00897B},
{'#43A047': 0xFF43A047},
{'#7CB342': 0xFF7CB342},
{'#C0CA33': 0xFFC0CA33},
];
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Container(
child: new ListView.builder(
itemCount: palette.length,
itemBuilder: (context, index) => new Hero(
tag: palette[index].keys.first,
child: new GestureDetector(
onTap: () {
Navigator
.of(context)
.push(new ColorPageRoute(palette[index]));
},
child: new Container(
height: 64.0,
width: double.infinity,
color: new Color(palette[index].values.first),
child: new Center(
child: new Hero(
tag: 'text-${palette[index].keys.first}',
child: new Text(
palette[index].keys.first,
style: Theme.of(context).textTheme.title.copyWith(
color: Colors.white,
),
),
),
),
),
),
)),
),
);
}
}
class SecondPage extends StatelessWidget {
final Map<String, int> color;
SecondPage({this.color});
#override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('Color'),
),
body: new Hero(
tag: color.keys.first,
child: new Container(
color: new Color(color.values.first),
child: new Center(
child: new Hero(
tag: 'text-${color.keys.first}',
child: new Text(
color.keys.first,
style:
Theme.of(context).textTheme.title.copyWith(color: Colors.white),
),
),
),
),
),
);
}
}
class ColorPageRoute extends MaterialPageRoute {
ColorPageRoute(Map<String, int> color)
: super(
builder: (context) => new SecondPage(
color: color,
));
#override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return FadeTransition(opacity: animation, child: child);
}
}
Someone wrote an amazing dart-package for just this purpose: https://pub.dev/packages/morpheus#-readme-tab-
All you then need to do is use the MorpheusPageRoute and the package handles the rest.
...
Navigator.push(
context,
MorpheusPageRoute(
builder: (context) => MyWidget(title: title),
),
);
...
I'd just cheat and wrap the whole thing in a Stack - bottom layer would be a page with the AppBar, and the top layer would be transparent until painted on.
onTap, duplicate ListTile onto the top surface, and then a Hero animation would fill the full screen. It's not very elegant, but the framework doesn't (yet) provide for covering the AppBar easily, so having a canvas ready to be painted on for other tricky animations might be resourceful.
I'm unable to comment or edit Lucas' post (new account) but you also need to provide the parentKey of the widget where the animation is to begin:
final widgetKey = GlobalKey();
...
ListTile(
key: widgetKey,
title: Text('My ListItem'),
onTap: () => Navigator.push(
context,
MorpheusPageRoute(
builder: (context) => MyNewPage(),
parentKey: widgetKey,
),
),
),
https://pub.dev/packages/morpheus

How to make copyable Text Widget in Flutter?

When long tab on Text widget, a tooltip show up with 'copy'. When click on the 'copy' the text content should copy to system clipboard.
The following will copy the text on long tap, but does not show up 'copy', so user will not know, the content is copied to the clipboard.
class CopyableText extends StatelessWidget {
final String data;
final TextStyle style;
final TextAlign textAlign;
final TextDirection textDirection;
final bool softWrap;
final TextOverflow overflow;
final double textScaleFactor;
final int maxLines;
CopyableText(
this.data, {
this.style,
this.textAlign,
this.textDirection,
this.softWrap,
this.overflow,
this.textScaleFactor,
this.maxLines,
});
#override
Widget build(BuildContext context) {
return new GestureDetector(
child: new Text(data,
style: style,
textAlign: textAlign,
textDirection: textDirection,
softWrap: softWrap,
overflow: overflow,
textScaleFactor: textScaleFactor,
maxLines: maxLines),
onLongPress: () {
Clipboard.setData(new ClipboardData(text: data));
},
);
}
}
Since Flutter 1.9 you can use
SelectableText("Lorem ipsum...")
When text is selected the "Copy" context button will appear.
You can use a SnackBar to notify the user about the copy.
Here is a relevant code:
String _copy = "Copy Me";
#override
Widget build(BuildContext context) {
final key = new GlobalKey<ScaffoldState>();
return new Scaffold(
key: key,
appBar: new AppBar(
title: new Text("Copy"),
centerTitle: true,
),
body:
new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new GestureDetector(
child: new Text(_copy),
onLongPress: () {
Clipboard.setData(new ClipboardData(text: _copy));
key.currentState.showSnackBar(
new SnackBar(content: new Text("Copied to Clipboard"),));
},
),
new TextField(
decoration: new InputDecoration(hintText: "Paste Here")),
]),
);
}
EDIT
I was working on something and I did the followin, so I thought of revisiting this answer:
import "package:flutter/material.dart";
import 'package:flutter/services.dart';
void main() {
runApp(new MaterialApp(home: new MyApp(),
));
}
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => new _MyAppState();
}
class _MyAppState extends State<MyApp> with TickerProviderStateMixin {
String _copy = "Copy Me";
#override
Widget build(BuildContext context) {
final key = new GlobalKey<ScaffoldState>();
return new Scaffold(
key: key,
appBar: new AppBar(
title: new Text("Copy"),
centerTitle: true,
),
body:
new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new GestureDetector(
child: new CustomToolTip(text: "My Copyable Text"),
onTap: () {
},
),
new TextField(
decoration: new InputDecoration(hintText: "Paste Here")),
]),
);
}
}
class CustomToolTip extends StatelessWidget {
String text;
CustomToolTip({this.text});
#override
Widget build(BuildContext context) {
return new GestureDetector(
child: new Tooltip(preferBelow: false,
message: "Copy", child: new Text(text)),
onTap: () {
Clipboard.setData(new ClipboardData(text: text));
},
);
}
}
There is also list of properties it in SelectableText to enable option copy, paste, selectAll, cut
child: Center(
child: SelectableText('Hello Flutter Developer',
cursorColor: Colors.red,
showCursor: true,
toolbarOptions: ToolbarOptions(
copy: true,
selectAll: true,
cut: false,
paste: false
),
style: Theme.of(context).textTheme.body2)
),
SelectableText widget
const SelectableText(
this.data, {
Key key,
this.focusNode,
this.style,
this.strutStyle,
this.textAlign,
this.textDirection,
this.showCursor = false,
this.autofocus = false,
ToolbarOptions toolbarOptions,
this.maxLines,
this.cursorWidth = 2.0,
this.cursorRadius,
this.cursorColor,
this.dragStartBehavior = DragStartBehavior.start,
this.enableInteractiveSelection = true,
this.onTap,
this.scrollPhysics,
this.textWidthBasis,
})
SelectableText(
"Copy me",
onTap: () {
// you can show toast to the user, like "Copied"
},
)
If you want to have different styling for text, use
SelectableText.rich(
TextSpan(
children: [
TextSpan(text: "Copy me", style: TextStyle(color: Colors.red)),
TextSpan(text: " and leave me"),
],
),
)
I use Clipboard.setData inside function.
...
child: RaisedButton(
onPressed: (){
Clipboard.setData(ClipboardData(text: "$textcopy"));
},
disabledColor: Colors.blue[400],
child: Text("Copy", style: TextStyle(color: Colors.white),),
),
I created a helper class CopiableText to accomplish my job. Just copy the class from below and put it in your code.
Helper class
copiable_text_widget.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class CopiableText extends StatelessWidget {
final String text;
final String copyMessage;
final Widget child;
CopiableText(this.text, {this.copyMessage, this.child});
#override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
child: InkWell(
onTap: () {
Scaffold.of(context).showSnackBar(SnackBar(
content: Text(this.copyMessage ?? 'Copied to clipboard'),
));
Clipboard.setData(new ClipboardData(text: this.text));
},
child: Align(
alignment: Alignment.centerLeft,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 2),
child: this.child ??
Text(
this.text,
style: TextStyle(color: Color(0xFF1E272E), fontSize: 14),
),
),
),
),
);
}
}
Use it in different ways
import 'package:chaincargo_courier/ui/widgets/copiable_text_widget.dart';
import 'package:flutter/material.dart';
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Container(
child: Column(
children: [
// Just straightforward, click to copy
CopiableText('You are awesome'),
// Give a custom confirmation message
CopiableText(
'Asia, Nepal, Biratnagar',
copyMessage: 'Address copied to clipboard',
),
// Set custom child
CopiableText(
'Stunning view of mount everest',
copyMessage: 'Caption copied to clipboard',
child: Column(
children: [
Image.network(
'https://cdn.pixabay.com/photo/2010/11/29/mount-everest-413_960_720.jpg',
errorBuilder: (BuildContext context, Object exception,
StackTrace stackTrace) {
return Text('Cannot load picture');
},
),
Text('Stunning view of mount everest')
],
),
),
],
),
);
}
}
Just use SelectableText
SelectableText(
iosInfo.identifierForVendor.toString(),
),
Support Links and Copy&Paste
If you want to support both Links and Copy&Paste, use the SelectableLinkify widget.
This widget is part of the flutter_linkify package.
SelectableLinkify(
text: "Made by https://cretezy.com\n\nMail: example#gmail.com",
);

Resources