Skip to content

Latest commit

 

History

History
456 lines (389 loc) · 11.4 KB

09. Flutter用户交互 - 导航和路由.md

File metadata and controls

456 lines (389 loc) · 11.4 KB

导航和路由 Navigation & routing

导航到新的页面和返回 Navigate to a new screen and back

大部分app包含多个屏幕来展示不同类型的信息. 如, 一个app可能会有一个屏幕用于展示产品. 用户可以点击产品图进入一个新的屏幕, 来获取详细信息.

术语: Flutter中screens 和 pages 称为routes(路由).

安卓中, route等同于Activity. iOS中, route等同于ViewController. Flutter中, route只是一个widget. 使用Navigator导航到一个新的route.

Directions

通过下面步骤在两个路由的导航

    1. 创建两个路由 Create two routes
    1. 使用Navigator.push()导航到第二个路由 Navigate to the second route using Navigator.push()
    1. 使用Navigator.pop()返回第一个路由 Return to the first route using Navigator.pop()
1. Create two routes

创建两个路由, 每个路由有一个按钮. 点击第一个路由的按钮跳转到第二个路由. 点击第二个路由的按钮, 返回第一个路由.

class FirstRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Route'),
      ),
      body: Center(
        child: RaisedButton(
          child: Text('Open route'),
          onPressed: () {
            // Navigate to second route when tapped.
          },
        ),
      ),
    );
  }
}

class SecondRoute extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second Route"),
      ),
      body: Center(
        child: RaisedButton(
          onPressed: () {
            // Navigate back to first route when tapped.
          },
          child: Text('Go back!'),
        ),
      ),
    );
  }
}
2. Navigate to the second route using Navigator.push()

使用Navigator.push()方法, 来转到新的路由. push()方法将一个Route加入Navigator管理的stack中. 可以使用自己的Route, 也可以使用MaterialPageRoute. MaterialPageRoute很便利, 因为使用按照特定平台的动画.

// Within the `FirstRoute` widget
onPressed: () {
  Navigator.push(
    context,
    MaterialPageRoute(builder: (context) => SecondRoute()),
  );
}
3. Return to the first route using Navigator.pop()

使用Navigator.pop()关闭第二个路由, 并返回第一个.pop()方法将当前路由从navigator管理的stack中移除.

// Within the SecondRoute widget
onPressed: () {
  Navigator.pop(context);
}

传递数据到新的页面 Send data to a new screen

通常, 我们会希望导航到新页面时, 能传递一些数据过去.

记住: Screen只是Widget. 该案例中, 会创建一个Todo列表. 当点击一个todo, 跳转到新的Screen(Widget), 把todo的信息显示出来.

Directions

    1. 定义Todo类
    1. 显示Todo列表
    1. 创建详情页, 来显示todo信息
    1. 导航并传递数据到详情页
1. Define a Todo class
class Todo {
  final String title;
  final String description;

  Todo(this.title, this.description);
}
2. Create a List of Todos

生成20个todo对象, 显示到ListView上

Generate the List of Todos
final todos = List<Todo>.generate(
  20,
  (i) => Todo(
        'Todo $i',
        'A description of what needs to be done for Todo $i',
      ),
);
Display the List of Todos using a ListView
ListView.builder(
  itemCount: todos.length,
  itemBuilder: (context, index) {
    return ListTile(
      title: Text(todos[index].title),
    );
  },
);
3. Create a Detail Screen that can display information about a todo
4. Navigate and pass data to the Detail Screen

完整案例

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';

class Todo {
  final String title;
  final String description;

  Todo(this.title, this.description);
}

void main() {
  runApp(MaterialApp(
    title: 'Passing Data',
    home: TodosScreen(
      todos: List.generate(
        20,
        (i) => Todo(
              'Todo $i',
              'A description of what needs to be done for Todo $i',
            ),
      ),
    ),
  ));
}

class TodosScreen extends StatelessWidget {
  final List<Todo> todos;

  TodosScreen({Key key, @required this.todos}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Todos'),
      ),
      body: ListView.builder(
        itemCount: todos.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(todos[index].title),
            // When a user taps on the ListTile, navigate to the DetailScreen.
            // Notice that we're not only creating a DetailScreen, we're
            // also passing the current todo through to it!
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => DetailScreen(todo: todos[index]),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

class DetailScreen extends StatelessWidget {
  // Declare a field that holds the Todo
  final Todo todo;

  // In the constructor, require a Todo
  DetailScreen({Key key, @required this.todo}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // Use the Todo to create our UI
    return Scaffold(
      appBar: AppBar(
        title: Text(todo.title),
      ),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Text(todo.description),
      ),
    );
  }
}

返回数据到上一页 Return data from a screen

有些情况下, 需要将数据返回上一页. 例如, 跳转到的一个页面, 该页面中有两个选项. 当用户点击其中一个, 希望可以告知上一个页面用户的选择是哪个, 来做对应的操作.

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    title: 'Returning Data',
    home: HomeScreen(),
  ));
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Returning Data Demo'),
      ),
      body: Center(child: SelectionButton()),
    );
  }
}

class SelectionButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      onPressed: () {
        _navigateAndDisplaySelection(context);
      },
      child: Text('Pick an option, any option!'),
    );
  }

  // A method that launches the SelectionScreen and awaits the result from
  // Navigator.pop!
  _navigateAndDisplaySelection(BuildContext context) async {
    // Navigator.push returns a Future that will complete after we call
    // Navigator.pop on the Selection Screen!
    final result = await Navigator.push(
      context,
      MaterialPageRoute(builder: (context) => SelectionScreen()),
    );

    // After the Selection Screen returns a result, hide any previous snackbars
    // and show the new result!
    Scaffold.of(context)
      ..removeCurrentSnackBar()
      ..showSnackBar(SnackBar(content: Text("$result")));
  }
}

class SelectionScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Pick an option'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: RaisedButton(
                onPressed: () {
                  // Close the screen and return "Yep!" as the result
                  Navigator.pop(context, 'Yep!');
                },
                child: Text('Yep!'),
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: RaisedButton(
                onPressed: () {
                  // Close the screen and return "Nope!" as the result
                  Navigator.pop(context, 'Nope.');
                },
                child: Text('Nope.'),
              ),
            )
          ],
        ),
      ),
    );
  }
}

导航到指定路由 Navigate with named routes

给路由命名, 使用Navigator.pushNamed导航到指定的页面.

import 'package:flutter/material.dart';

void main() {
  runApp(MaterialApp(
    title: 'Named Routes Demo',
    // Start the app with the "/" named route. In our case, the app will start
    // on the FirstScreen Widget
    initialRoute: '/',
    routes: {
      // When we navigate to the "/" route, build the FirstScreen Widget
      '/': (context) => FirstScreen(),
      // When we navigate to the "/second" route, build the SecondScreen Widget
      '/second': (context) => SecondScreen(),
    },
  ));
}

class FirstScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('First Screen'),
      ),
      body: Center(
        child: RaisedButton(
          child: Text('Launch screen'),
          onPressed: () {
            // Navigate to the second screen using a named route
            Navigator.pushNamed(context, '/second');
          },
        ),
      ),
    );
  }
}

class SecondScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second Screen"),
      ),
      body: Center(
        child: RaisedButton(
          onPressed: () {
            // Navigate back to the first screen by popping the current route
            // off the stack
            Navigator.pop(context);
          },
          child: Text('Go back!'),
        ),
      ),
    );
  }
}

页面转场动画 Animating a widget across screens

在两个页面中展示同一张图. 当用户点击第一个页面的图时, 通过动画衔接跳转第二个页面.

为了使用动画衔接两个页面之间的跳转, 需要将两个页面的Image Widget包裹在一个Hero widget中. Hero需要两个参数:

  • tag: Hero对象的标识. 两个页面必须保持一致.
  • child: 希望在页面间做动画的Widget.

可以使用可重用的Hero对象, 以下案例为了演示方便重写了.

import 'package:flutter/material.dart';

void main() => runApp(HeroApp());

class HeroApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Transition Demo',
      home: MainScreen(),
    );
  }
}

class MainScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Main Screen'),
      ),
      body: GestureDetector(
        child: Hero(
          tag: 'imageHero',
          child: Image.network(
            'https://picsum.photos/250?image=9',
          ),
        ),
        onTap: () {
          Navigator.push(context, MaterialPageRoute(builder: (_) {
            return DetailScreen();
          }));
        },
      ),
    );
  }
}

class DetailScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: GestureDetector(
        child: Center(
          child: Hero(
            tag: 'imageHero',
            child: Image.network(
              'https://picsum.photos/250?image=9',
            ),
          ),
        ),
        onTap: () {
          Navigator.pop(context);
        },
      ),
    );
  }
}