Skip to content

Commit

Permalink
Support on-screen keyboard (#9)
Browse files Browse the repository at this point in the history
This PR includes changes and new features to address the issue of the
keyboard overlap on the sheet. Once the PR is merged, the framework will
automatically offset the sheet by the keyboard height to prevent the
content from being overlapped by the keyboard.

## Changes made

### Breaking changes

- Change the type of `SheetMetrics.viewportDimensions` from `Size` to
`ViewportDimensions`, which holds the `MediaQueryData.viewInsets` of the
window (typically the keyboard height) in addition to the viewport size.

### Internal changes

- Change `SheetViewport` to gradually offset the sheet by the keyboard
height.
- Split `_ContentScrollDrivenSheetActivity` into the 3 parts according
to the lifecycle; `_ContentIdleScrollSheetActivity`,
`_ContentUserScrollSheetActivity` and
`_ContentBallisticScrollSheetActivity`.
- Change some sheet activities such as `UserDragSheetActivity` and
`_ContentUserScrollSheetActivity` to keep the visual position of the
sheet unchanged while a keyboard dismissing/appearing animation is
running.

### New features

- Add `resizeToAvoidBottomInset` property to `SheetContentScaffold`.
- Fixes #5.

### Documentation

- Fixes #2.
- Add a tutorial for `SheetKeyboardDismissBehavior`.
- Add the description of `SheetKeyboardDismissBehavior` to the README.
  • Loading branch information
fujidaiti authored Jan 28, 2024
1 parent 0f3788e commit fd4e84f
Show file tree
Hide file tree
Showing 28 changed files with 1,867 additions and 135 deletions.
14 changes: 14 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@
"program": "lib/showcase/airbnb_mobile_app.dart",
"cwd": "./cookbook"
},
{
"name": "Todo List",
"request": "launch",
"type": "dart",
"program": "lib/showcase/todo_list/main.dart",
"cwd": "./cookbook"
},
{
"name": "Scrollable Sheet",
"request": "launch",
Expand Down Expand Up @@ -94,6 +101,13 @@
"type": "dart",
"program": "lib/tutorial/extent_driven_animation.dart",
"cwd": "./cookbook"
},
{
"name": "Keyboard Dismiss Behavior",
"request": "launch",
"type": "dart",
"program": "lib/tutorial/keyboard_dismiss_behavior.dart",
"cwd": "./cookbook"
}
]
}
12 changes: 12 additions & 0 deletions Makefile.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
[config]
skip_core_tasks = true

[env]
PROJECT_ROOT = { script = ["pwd"] }
SCRIPTS_DIR = "${PROJECT_ROOT}/scripts"

[tasks.flutter-build]
script = '''
flutter clean
Expand All @@ -9,8 +13,12 @@ flutter pub get

[tasks.flutter-analyze]
script = '''
echo "Running dart format"
dart format . -o none --set-exit-if-changed
echo "Running dart analyze"
dart analyze
echo "Running disallowed patterns check"
bash $SCRIPTS_DIR/pattern_checker.sh "*.dart" "--" "debugPrint"
'''

[tasks.flutter-check]
Expand All @@ -19,17 +27,21 @@ run_task = { name = ['flutter-build', 'flutter-analyze'] }
[tasks.build-all]
script_runner = "@duckscript"
script = '''
echo "Building package"
cd ./package
cm_run_task flutter-build
echo "Building cookbook"
cd ../cookbook
cm_run_task flutter-build
'''

[tasks.check-all]
script_runner = "@duckscript"
script = '''
echo "Running flutter-check for package"
cd ./package
cm_run_task flutter-check
echo "Running flutter-check for cookbook"
cd ../cookbook
cm_run_task flutter-check
'''
3 changes: 0 additions & 3 deletions cookbook/lib/showcase/todo_list.dart

This file was deleted.

171 changes: 171 additions & 0 deletions cookbook/lib/showcase/todo_list/main.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import 'package:cookbook/showcase/todo_list/models.dart';
import 'package:cookbook/showcase/todo_list/todo_editor.dart';
import 'package:flutter/material.dart';

void main() {
runApp(const _TodoListExample());
}

class _TodoListExample extends StatelessWidget {
const _TodoListExample();

@override
Widget build(BuildContext context) {
return const MaterialApp(home: _Home());
}
}

class _Home extends StatefulWidget {
const _Home();

@override
State<_Home> createState() => _HomeState();
}

class _HomeState extends State<_Home> {
late final TodoList _todoList;

@override
void initState() {
super.initState();
_todoList = TodoList();
}

@override
void dispose() {
_todoList.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Todo List'),
),
body: _TodoListView(todoList: _todoList),
floatingActionButton: FloatingActionButton(
onPressed: () => addTodo(),
child: const Icon(Icons.add),
),
);
}

Future<void> addTodo() async {
final todo = await showTodoEditor(context);
if (todo != null) {
_todoList.add(todo);
}
}
}

class _TodoListView extends StatelessWidget {
const _TodoListView({
required this.todoList,
});

final TodoList todoList;

@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: todoList,
builder: (context, _) {
return ListView.separated(
itemCount: todoList.length,
itemBuilder: (context, index) {
return _TodoListViewItem(
todo: todoList[index],
checkboxCallback: (value) => todoList.toggle(index),
);
},
separatorBuilder: (context, index) {
return const Divider(indent: 24);
},
);
},
);
}
}

class _TodoListViewItem extends StatelessWidget {
const _TodoListViewItem({
Key? key,
required this.todo,
required this.checkboxCallback,
}) : super(key: key);

final Todo todo;
final ValueChanged<bool?> checkboxCallback;

@override
Widget build(BuildContext context) {
final statuses = <Widget>[
if (todo.priority != Priority.none)
_StatusChip(
icon: Icons.flag,
color: todo.priority.color,
label: todo.priority.displayName,
),
];

final description = switch (todo.description) {
null => null,
final text => Text(
text,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context)
.textTheme
.bodyLarge
?.copyWith(color: Colors.black54),
),
};

final secondaryContent = [
if (description != null) ...[description, const SizedBox(height: 8)],
if (statuses.isNotEmpty && !todo.isDone) Wrap(children: statuses),
];

return CheckboxListTile(
value: todo.isDone,
controlAffinity: ListTileControlAffinity.leading,
onChanged: checkboxCallback,
title: Text(todo.title),
isThreeLine: secondaryContent.isNotEmpty,
subtitle: switch (secondaryContent.isNotEmpty) {
true => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: secondaryContent,
),
false => null,
},
);
}
}

class _StatusChip extends StatelessWidget {
const _StatusChip({
required this.icon,
required this.color,
required this.label,
});

final IconData icon;
final Color? color;
final String label;

@override
Widget build(BuildContext context) {
return Chip(
avatar: Icon(icon, color: color),
label: Text(label),
padding: EdgeInsets.zero,
labelPadding: const EdgeInsets.only(right: 12),
visualDensity: const VisualDensity(
vertical: -4,
horizontal: -4,
),
);
}
}
55 changes: 55 additions & 0 deletions cookbook/lib/showcase/todo_list/models.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';

// TODO: Add 'reminder' and 'due date' fields.
class Todo {
final String title;
final String? description;
final Priority priority;
final bool isDone;

const Todo({
required this.title,
this.description,
this.priority = Priority.none,
this.isDone = false,
});
}

enum Priority {
high(displayName: 'High Priority', color: Colors.red),
medium(displayName: 'Medium Priority', color: Colors.orange),
low(displayName: 'Low Priority', color: Colors.blue),
none(displayName: 'No Priority');

const Priority({
required this.displayName,
this.color,
});

final String displayName;
final Color? color;
}

class TodoList extends ChangeNotifier {
final List<Todo> _todos = [];

int get length => _todos.length;

Todo operator [](int index) => _todos[index];

void add(Todo todo) {
_todos.insert(0, todo);
notifyListeners();
}

void toggle(int index) {
final todo = _todos[index];
_todos[index] = Todo(
title: todo.title,
description: todo.description,
priority: todo.priority,
isDone: !todo.isDone,
);
notifyListeners();
}
}
Loading

0 comments on commit fd4e84f

Please sign in to comment.