Github project address: Project address
The last blog has completed the construction of the home page basic controls, but there is still a lack of interaction, and there are only three fixed recommendation cards. In this blog, we will improve the home page of Meituan together.
AppBar adds button click events and pop-up menus
/// Main interface AppBar
AppBar _buildHomeAppBar(a) {
return AppBar(
automaticallyImplyLeading: false,
elevation: 0.0,
backgroundColor: Colors.white,
flexibleSpace: SafeArea(
// Fit bangs
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
InkWell(
onTap: () => Navigator.of(context)
.push(CupertinoPageRoute(builder: (context) => TestPage())),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: ClipOval(
child: Image.asset("images/protrait.png",
width: 35.0, height: 35.0),
),
),
),
InkWell(
onTap: () => Navigator.of(context)
.push(CupertinoPageRoute(builder: (context) => TestPage())),
child: Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
"Three rivers",
style: TextStyle(color: Colors.black, fontSize: 15.0),
),
Icon(
Icons.keyboard_arrow_down,
size: 15.0,
),
],
),
Text(
"Sunny 20 °",
style: TextStyle(fontSize: 10.0),
)
],
),
padding: const EdgeInsets.all(8.0),
),
),
Expanded(
child: GestureDetector(
onTap: () {
Navigator.of(context).push(
CupertinoPageRoute(builder: (context) => SearchPage()));
},
child: Container(
height: 45.0,
child: Card(
elevation: 0.0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0))),
color: Colors.grey[200],
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.search,
color: Colors.black87,
size: 20.0,
),
Text(
"Barbecue buffet",
style:
TextStyle(fontSize: 15.0, color: Colors.black87),
),
],
),
),
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: PopupMenuButton(
child: Icon(
Icons.add,
size: 30,
color: Colors.black,
),
itemBuilder: (context) => <PopupMenuEntry>[
PopupMenuItem(
child: Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(Icons.image),
SizedBox(
width: 20,
),
Text("Sweep it."),
],
)),
),
PopupMenuItem(
child: Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(Icons.scanner),
SizedBox(
width: 20,
),
Text("Payment code")],),),),),),),),),); }Copy the code
The main structure remains the same, with the controls arranged in order: a circular picture box, a clickable text with location and weather, a search box for navigation, and a pop-up menu button. In the last blog post, we finished drawing the styles and layouts, but this is about adding user-clickable functionality to the controls. As mentioned earlier, Flutter provides many styles of Button controls. You can also add the ability to capture user gestures through GestureDetector. This time we did it with another control, InkWell. The Button class that comes with Flutter usually has water ripples and highlights when selected, but they take up a lot of space and are not suitable for the compact layout style of Domestic apps. While using GestureDetector control to capture gestures, there is no animation when clicking, so the interaction is poor. The InkWell control solves these problems by allowing child controls to capture various gestures while animating water waves in line with Google Material Design without taking up extra space. Take a look at the constructor of the InkWell control.
const InkWell({
Key key,
Widget child,
GestureTapCallback onTap,
GestureTapCallback onDoubleTap,
GestureLongPressCallback onLongPress,
GestureTapDownCallback onTapDown,
GestureTapCancelCallback onTapCancel,
ValueChanged<bool> onHighlightChanged,
Color highlightColor,
Color splashColor,
InteractiveInkFeatureFactory splashFactory,
double radius,
BorderRadius borderRadius,
ShapeBorder customBorder,
bool enableFeedback = true,
bool excludeFromSemantics = false,})
Copy the code
Key, as mentioned earlier, is used to identify different objects of the same type of control on widget Tree. It is not normally used. OnTap and onLongPress are examples of gestures that InkWell controls can capture, and when the value of these events is not empty, the corresponding gesture triggers the water ripple effect. The value of the splashColor property is the color of the water ripple, the highlightColor corresponds to the color when highlighted (triggered when selected, clicked, etc.), and the borderRadius property sets the arc of the border when the water ripple is triggered.
When using this control and adding the corresponding gesture event does not show the water ripple effect, then wrap an Ink control.
InkWell(
borderRadius: BorderRadius.circular(45),
onTap: () => Navigator.of(context)
.push(CupertinoPageRoute(builder: (context) => TestPage())),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: ClipOval(
child: Image.asset("images/protrait.png",
width: 35.0, height: 35.0),),),),Copy the code
Wrap an InkWell control around both the round picture box and the address title, and navigate to a test screen in the click event. CupertinoPageRoute’s job is to animate the route to an ios-style slide in on the left. BorderRadius. Circular (45) sets the radians of rounded corners in the four directions of upper left, upper right, lower left and lower right to 45°. While the search box does not require water ripple effects, we use the GestureDetector control to capture click events.
Popup menu button
Padding(
padding: const EdgeInsets.all(8.0),
child: PopupMenuButton(
child: Icon(
Icons.add,
size: 30,
color: Colors.black,
),
itemBuilder: (context) => <PopupMenuEntry>[
PopupMenuItem(
child: Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(Icons.image),
SizedBox(
width: 20,
),
Text("Sweep it."),
],
)),
),
PopupMenuItem(
child: Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(Icons.scanner),
SizedBox(
width: 20,
),
Text("Payment code"() [[() [[() [() [()Copy the code
Flutter has a button control that handles pop-up menus: PopupMenuButton. Constructors are as follows:
const PopupMenuButton({
Key key,
@required thisItemBuilder, // Menu item to display when the button is clickedthisInitialValue, // initialValuethis.onselected, // Click the eventthis.oncanceled, // Cancels when the user clicks on another regionthis.tooltip, // Displays prompt information at a long timethis.elevation = 8.0// Shadow heightthis.padding = const EdgeInsets.all(8.0)./ / from the outside
this.child, / / the child controls
this.icon, / / icon
this.offset = Offset.zero,
})
Copy the code
ItemBuilder accepts a list of PopupMenuEntry
. Dynamic can be replaced with any type as the value returned when a menu item is clicked, usually as a string or integer type. The itemBudiler argument cannot be empty, the child and icon arguments cannot be given at the same time, and a default menu button is displayed when both are empty.
Each PopupMenuItem is arranged vertically as a separate menu item. The child parameter receives a control as what is displayed, the value parameter as what is returned when clicked, and the Enabled parameter indicates whether it is enabled.
const PopupMenuItem({
Key key,
this.value,
this.enabled = true.this.height = _kMenuItemHeight,
@required this.child,
})
Copy the code
Effect:
Dynamic recommended card list
As mentioned earlier, the ListView is the most common scroll block in a Flutter and can be set to scroll vertically or horizontally using the scrollDirection parameter. Use Builder mode (named constructor) when an infinite or long scrolling list is required. Use itemBuilder to dynamically load child controls to display. A simple example:
ListView.builder(
itemCount: 20,
itemBuilder: (context, index) => Text(index.toString()),
);
Copy the code
You can display a fixed length scroll list using the itemCount and itemBuilder parameters or you can display an infinite scroll list using just the itemBuilder parameter. Because we want to show an animation when deleting recommendation cards and loading more, we use an AnimatedList control instead of listView.Builder. The AnimatedList control usually requires a GlobalKey
to animate its child controls. In general, we place the key in the State property so that we can control the drawn list anywhere in the class.
Displaying an AnimatedList usually requires building a list of widgets that add and remove child controls at the same time as calling the add and remove methods in the key. We also need to listen for more recommendation cards to load as the list slides to the bottom, so we need a ScrollController that controls the scrolling of the AnimatedList.
First, add GlobalKey
, ScrollController, List
to the State property.
class _HomePageState extends State<HomePage> {
final GlobalKey<AnimatedListState> _listKey =
new GlobalKey<AnimatedListState>();
List<Widget> _list;
ScrollController _controller;
bool isRefreshing; // Avoid loading more listeners at load time.Copy the code
Override the initState method and add add delete controls and display a method to remove the animation.
@override
void initState(a) {
super.initState(); // Call the initState method of the parent class
isRefreshing = false;
_controller = ScrollController();
_controller.addListener(() {
// Add a listener to ScrollController
if (_controller.position.pixels == _controller.position.maxScrollExtent &&
!this.isRefreshing) {
// When scrolling to the bottom and not being loaded
setState(() {
isRefreshing = true; // Set the current state to loading
Widget item = Container(
// An ios-style loading indicator
height: 50,
child: Center(child: CupertinoActivityIndicator()),
);
_selectedItem = item; // Set the currently selected control as the indicator, so that the indicator can be deleted after loading
_insertItem(_list.length, item); // Add the indicator to the bottom
});
Future.delayed(Duration(seconds: 2), () {
// Execute the method passed in after two seconds
_removeItem(_list.length - 1); // Remove the indicator and add three recommendation cards
_insertItem(_list.length, _buildCard1());
_insertItem(_list.length, _buildCard2());
_insertItem(_list.length, _buildCard3());
setState(() {
isRefreshing = false; // Set the current state to not being loaded}); }); }}); _list = <Widget>[// Initialize three recommendation cards the first time a HomePage is loaded
_buildCard1(),
_buildCard2(),
_buildCard3(),
];
}
void _removeItem(int index) {
_list.removeAt(index); // Remove the control at the index position from the control list
_listKey.currentState.removeItem(
// Remove the control corresponding to the index position from the key and display an animation of the removal using the _buildRemovedItem method
index + 6,
(context, animation) =>
_buildRemovedItem(_selectedItem, context, animation));
}
void _insertItem(int index, Widget item) {
_list.insert(index, item); // Add a control from the control list and key
_listKey.currentState.insertItem(index + 6);
}
// Build the removed control (show the removed animation)
Widget _buildRemovedItem(
Widget widget, BuildContext context, Animation<double> animation) {
return SizeTransition( // Encapsulates a control with a sizing animation
axis: Axis.vertical, // Dimension animation scaling direction
sizeFactor: animation, // Size value, which is the value given by AnimatedList to remove the animation
child: widget,
);
}
Copy the code
Add an AnimatedList control to the body of the Scaffold and wrap a gradient Container around it.
@override
Widget build(BuildContext context) {
final bodys = _initBody(); // The top three lines of controls, such as the title bar and the wheel chart, which cannot be removed, contain SizedBox controls for separation, so the total length is 6
return Scaffold(
appBar: _buildHomeAppBar(),
body: Container(
decoration: GradientDecoration, // Define gradient decorators in theme files
child: AnimatedList(
controller: _controller, // Bind the scroll controller, key, etc
key: _listKey,
initialItemCount: bodys.length + _list.length, // The total length of the control at initialization
itemBuilder: (context, index, animation) { // This method is called to build the control when the child control is to be visible
if (index > 5) { // Index greater than 5 displays recommended cards in _list, otherwise controls in bodys.
return _buildItem(context, index - bodys.length, animation);
} else {
returnbodys[index]; }},),),); }Copy the code
Add an ios-style loading indicator at the bottom when listening to the list scroll to the bottom, remove the indicator after two seconds and add three recommendation card buttons to the bottom.
When you click the delete button on the upper right of the recommendation card, delete the card from the list and display a delete animation effect. Start by adding an onDelete attribute to the constructor of the recommendation card to receive the delete button click callback event. Pass the method in the delete button click event.
typedef OnPressCallback = void Function(Widget selectedItem);
// Define a method that takes a Widget and returns no value
class ScenicCard extends StatelessWidget {
ScenicCard(
{@required this.price,
@required this.title,
@required this.imageUrls,
@required this.score,
@required this.address,
this.onDelete, //new
this.tags = const <Widget>[]});
final Widget price;
final List<Widget> tags;
final String title;
final List<String> imageUrls;
final String score;
final String address;
final OnPressCallback onDelete; //new.return RecommendedCard(
title: title,
onDelete: () => onDelete(this),
child: Column(
...
Copy the code
The callback method is passed when a recommendation card is constructed in a HomePage.
ScenicCard(
onDelete: _showDeleteDialog,
...
// Displays a close dialog box for recommended cards
void _showDeleteDialog(Widget selectedItem) {
_selectedItem = selectedItem; // Updates the currently selected control to remove it
var dialog = SimpleDialog( // A simple dialog box
// Rounded
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
titlePadding: EdgeInsets.only(top: 20),
title: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
"Choosing specific reasons will reduce recommendations.",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
SizedBox(
height: 20,
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// A black rounded border button
_buildMyButton("Yes."),
SizedBox(
width: 10,
),
_buildMyButton("Not interested"),
SizedBox(
width: 10,
),
_buildMyButton("The price is not right"),
],
),
SizedBox(
height: 15,
),
GestureDetector(
onTap: () {
_removeItem(_list.indexOf(_selectedItem));
Navigator.of(context).pop(); // The dialog box is displayed
},
child: InkWell(
child: Container(
height: 50,
decoration: BoxDecoration(
color: CupertinoColors.lightBackgroundGray,
borderRadius: BorderRadius.only(
// The outer Card shape attribute does not restrict the inner Container, so you need to define the bottom arc for the bottom Container
bottomLeft: Radius.circular(15),
bottomRight: Radius.circular(15))),
child: Center(
child: Text(
"Not interested",
style: TextStyle(fontSize: 12, color: Colors.teal),
),
),
),
),
)
],
),
);
showDialog(
context: context,
builder: (context) => dialog,
);
}
Copy the code
The pull-up loading list is now complete, and the full project code can be found on Github