mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
commit
a67ceddd90
@ -67,6 +67,7 @@ dart_pkg("sky") {
|
||||
"lib/widgets/fixed_height_scrollable.dart",
|
||||
"lib/widgets/flat_button.dart",
|
||||
"lib/widgets/floating_action_button.dart",
|
||||
"lib/widgets/focus.dart",
|
||||
"lib/widgets/icon.dart",
|
||||
"lib/widgets/icon_button.dart",
|
||||
"lib/widgets/ink_well.dart",
|
||||
|
||||
@ -2,35 +2,47 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:sky/editing/input.dart';
|
||||
import 'package:sky/theme/colors.dart' as colors;
|
||||
import 'package:sky/theme/typography.dart' as typography;
|
||||
import 'package:sky/widgets/basic.dart';
|
||||
import 'package:sky/widgets/default_text_style.dart';
|
||||
import 'package:sky/widgets/theme.dart';
|
||||
import 'package:sky/widgets/widget.dart';
|
||||
import 'package:sky/widgets/task_description.dart';
|
||||
import 'package:sky/widgets/scaffold.dart';
|
||||
import 'package:sky/widgets/tool_bar.dart';
|
||||
import 'package:sky/widgets/icon_button.dart';
|
||||
import 'package:sky/widgets/floating_action_button.dart';
|
||||
import 'package:sky/widgets/focus.dart';
|
||||
import 'package:sky/widgets/icon.dart';
|
||||
import 'package:sky/widgets/icon_button.dart';
|
||||
import 'package:sky/widgets/material.dart';
|
||||
import 'package:sky/editing/input.dart';
|
||||
import 'package:sky/widgets/scaffold.dart';
|
||||
import 'package:sky/widgets/scrollable_viewport.dart';
|
||||
import 'package:sky/widgets/task_description.dart';
|
||||
import 'package:sky/widgets/theme.dart';
|
||||
import 'package:sky/widgets/tool_bar.dart';
|
||||
import 'package:sky/widgets/widget.dart';
|
||||
|
||||
class Field extends Component {
|
||||
Field({this.icon: null, this.placeholder: null});
|
||||
Field({
|
||||
Key key,
|
||||
this.inputKey,
|
||||
this.icon,
|
||||
this.placeholder
|
||||
}): super(key: key);
|
||||
|
||||
String icon;
|
||||
String placeholder;
|
||||
final GlobalKey inputKey;
|
||||
final String icon;
|
||||
final String placeholder;
|
||||
|
||||
Widget build() {
|
||||
return new Flex([
|
||||
new Padding(
|
||||
padding: const EdgeDims.symmetric(horizontal: 16.0),
|
||||
child: new Icon(type:icon, size:24)
|
||||
child: new Icon(type: icon, size: 24)
|
||||
),
|
||||
new Flexible(child:new Input(placeholder:placeholder, focused:false))
|
||||
new Flexible(
|
||||
child: new Input(
|
||||
key: inputKey,
|
||||
placeholder: placeholder
|
||||
)
|
||||
)
|
||||
],
|
||||
direction: FlexDirection.horizontal
|
||||
);
|
||||
@ -53,6 +65,13 @@ class AddressBookApp extends App {
|
||||
);
|
||||
}
|
||||
|
||||
static final GlobalKey nameKey = new GlobalKey();
|
||||
static final GlobalKey phoneKey = new GlobalKey();
|
||||
static final GlobalKey emailKey = new GlobalKey();
|
||||
static final GlobalKey addressKey = new GlobalKey();
|
||||
static final GlobalKey ringtoneKey = new GlobalKey();
|
||||
static final GlobalKey noteKey = new GlobalKey();
|
||||
|
||||
Widget buildBody() {
|
||||
return new Material(
|
||||
child: new ScrollableBlock([
|
||||
@ -62,12 +81,12 @@ class AddressBookApp extends App {
|
||||
decoration: new BoxDecoration(backgroundColor: colors.Purple[300])
|
||||
)
|
||||
),
|
||||
new Field(icon:"social/person", placeholder:"Name"),
|
||||
new Field(icon: "communication/phone", placeholder:"Phone"),
|
||||
new Field(icon: "communication/email", placeholder:"Email"),
|
||||
new Field(icon: "maps/place", placeholder:"Address"),
|
||||
new Field(icon: "av/volume_up", placeholder:"Ringtone"),
|
||||
new Field(icon: "content/add", placeholder:"Add note"),
|
||||
new Field(inputKey: nameKey, icon: "social/person", placeholder: "Name"),
|
||||
new Field(inputKey: phoneKey, icon: "communication/phone", placeholder: "Phone"),
|
||||
new Field(inputKey: emailKey, icon: "communication/email", placeholder: "Email"),
|
||||
new Field(inputKey: addressKey, icon: "maps/place", placeholder: "Address"),
|
||||
new Field(inputKey: ringtoneKey, icon: "av/volume_up", placeholder: "Ringtone"),
|
||||
new Field(inputKey: noteKey, icon: "content/add", placeholder: "Add note"),
|
||||
])
|
||||
);
|
||||
}
|
||||
@ -81,24 +100,25 @@ class AddressBookApp extends App {
|
||||
}
|
||||
|
||||
Widget build() {
|
||||
|
||||
ThemeData theme = new ThemeData(
|
||||
brightness: ThemeBrightness.light,
|
||||
primarySwatch: colors.Teal,
|
||||
accentColor: colors.PinkAccent[100]
|
||||
);
|
||||
|
||||
return new Theme(
|
||||
data: theme,
|
||||
child: new DefaultTextStyle(
|
||||
style: typography.error, // if you see this, you've forgotten to correctly configure the text style!
|
||||
child: new TaskDescription(
|
||||
label: 'Address Book',
|
||||
child: new DefaultTextStyle(
|
||||
style: typography.error, // if you see this, you've forgotten to correctly configure the text style!
|
||||
child: new TaskDescription(
|
||||
label: 'Address Book',
|
||||
child: new Focus(
|
||||
initialFocus: nameKey,
|
||||
child: buildMain()
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
|
||||
@ -10,6 +10,7 @@ import 'package:sky/widgets/drawer_divider.dart';
|
||||
import 'package:sky/widgets/drawer_header.dart';
|
||||
import 'package:sky/widgets/drawer_item.dart';
|
||||
import 'package:sky/widgets/floating_action_button.dart';
|
||||
import 'package:sky/widgets/focus.dart';
|
||||
import 'package:sky/widgets/icon.dart';
|
||||
import 'package:sky/widgets/icon_button.dart';
|
||||
import 'package:sky/widgets/modal_overlay.dart';
|
||||
@ -244,6 +245,8 @@ class StockHome extends StatefulComponent {
|
||||
);
|
||||
}
|
||||
|
||||
static GlobalKey searchFieldKey = new GlobalKey();
|
||||
|
||||
// TODO(abarth): Should we factor this into a SearchBar in the framework?
|
||||
Widget buildSearchBar() {
|
||||
return new ToolBar(
|
||||
@ -253,7 +256,7 @@ class StockHome extends StatefulComponent {
|
||||
onPressed: _handleSearchEnd
|
||||
),
|
||||
center: new Input(
|
||||
focused: true,
|
||||
key: searchFieldKey,
|
||||
placeholder: 'Search stocks',
|
||||
onChanged: _handleSearchQueryChanged
|
||||
),
|
||||
@ -318,6 +321,9 @@ class StockHome extends StatefulComponent {
|
||||
),
|
||||
];
|
||||
addMenuToOverlays(overlays);
|
||||
return new Stack(overlays);
|
||||
return new Focus(
|
||||
initialFocus: searchFieldKey,
|
||||
child: new Stack(overlays)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import 'package:sky/editing/editable_text.dart';
|
||||
import 'package:sky/mojo/keyboard.dart';
|
||||
import 'package:sky/painting/text_style.dart';
|
||||
import 'package:sky/widgets/basic.dart';
|
||||
import 'package:sky/widgets/focus.dart';
|
||||
import 'package:sky/widgets/theme.dart';
|
||||
|
||||
typedef void ValueChanged(value);
|
||||
@ -17,23 +18,18 @@ const EdgeDims _kTextfieldPadding = const EdgeDims.symmetric(vertical: 8.0);
|
||||
|
||||
class Input extends StatefulComponent {
|
||||
|
||||
// Current thinking is that Widget will have an optional globalKey
|
||||
// or heroKey and it will ask Focus.from(this).isFocused which will
|
||||
// check using its globalKey.
|
||||
// Only one element can use a globalKey at a time and its' up to
|
||||
// Widget.sync to maintain the mapping.
|
||||
// Never makes sense to have both a localKey and a globalKey.
|
||||
// Possibly a class HeroKey who functions as a UUID.
|
||||
|
||||
Input({Key key,
|
||||
this.placeholder,
|
||||
this.onChanged,
|
||||
this.focused})
|
||||
: super(key: key);
|
||||
Input({
|
||||
Key key,
|
||||
this.placeholder,
|
||||
this.onChanged
|
||||
}): super(key: key);
|
||||
|
||||
String placeholder;
|
||||
ValueChanged onChanged;
|
||||
bool focused = false;
|
||||
|
||||
String _value = '';
|
||||
EditableString _editableValue;
|
||||
KeyboardHandle _keyboardHandle = KeyboardHandle.unattached;
|
||||
|
||||
void initState() {
|
||||
_editableValue = new EditableString(
|
||||
@ -46,17 +42,13 @@ class Input extends StatefulComponent {
|
||||
void syncFields(Input source) {
|
||||
placeholder = source.placeholder;
|
||||
onChanged = source.onChanged;
|
||||
focused = source.focused;
|
||||
}
|
||||
|
||||
String _value = '';
|
||||
bool _isAttachedToKeyboard = false;
|
||||
EditableString _editableValue;
|
||||
|
||||
void _handleTextUpdated() {
|
||||
scheduleBuild();
|
||||
if (_value != _editableValue.text) {
|
||||
_value = _editableValue.text;
|
||||
setState(() {
|
||||
_value = _editableValue.text;
|
||||
});
|
||||
if (onChanged != null)
|
||||
onChanged(_value);
|
||||
}
|
||||
@ -64,10 +56,12 @@ class Input extends StatefulComponent {
|
||||
|
||||
Widget build() {
|
||||
ThemeData themeData = Theme.of(this);
|
||||
bool focused = Focus.at(this);
|
||||
|
||||
if (focused && !_isAttachedToKeyboard) {
|
||||
keyboard.show(_editableValue.stub);
|
||||
_isAttachedToKeyboard = true;
|
||||
if (focused && !_keyboardHandle.attached) {
|
||||
_keyboardHandle = keyboard.show(_editableValue.stub);
|
||||
} else if (!focused && _keyboardHandle.attached) {
|
||||
_keyboardHandle.release();
|
||||
}
|
||||
|
||||
TextStyle textStyle = themeData.text.subhead;
|
||||
@ -109,13 +103,23 @@ class Input extends StatefulComponent {
|
||||
|
||||
return new Listener(
|
||||
child: input,
|
||||
onPointerDown: (_) => keyboard.showByRequest()
|
||||
onPointerDown: focus
|
||||
);
|
||||
}
|
||||
|
||||
void focus(_) {
|
||||
if (Focus.at(this)) {
|
||||
assert(_keyboardHandle.attached);
|
||||
_keyboardHandle.showByRequest();
|
||||
} else {
|
||||
Focus.moveTo(this);
|
||||
// we'll get told to rebuild and we'll take care of the keyboard then
|
||||
}
|
||||
}
|
||||
|
||||
void didUnmount() {
|
||||
if (_isAttachedToKeyboard)
|
||||
keyboard.hide();
|
||||
if (_keyboardHandle.attached)
|
||||
_keyboardHandle.release();
|
||||
super.didUnmount();
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,15 +6,74 @@ import 'package:mojom/keyboard/keyboard.mojom.dart';
|
||||
import 'package:sky/mojo/shell.dart' as shell;
|
||||
|
||||
class _KeyboardConnection {
|
||||
KeyboardServiceProxy proxy;
|
||||
|
||||
_KeyboardConnection() {
|
||||
proxy = new KeyboardServiceProxy.unbound();
|
||||
shell.requestService("mojo:keyboard", proxy);
|
||||
}
|
||||
|
||||
KeyboardService get keyboard => proxy.ptr;
|
||||
KeyboardServiceProxy proxy;
|
||||
KeyboardService get keyboardService => proxy.ptr;
|
||||
|
||||
static final _KeyboardConnection instance = new _KeyboardConnection();
|
||||
}
|
||||
|
||||
final _KeyboardConnection _connection = new _KeyboardConnection();
|
||||
final KeyboardService keyboard = _connection.keyboard;
|
||||
class Keyboard {
|
||||
|
||||
Keyboard(this.service);
|
||||
|
||||
// The service is exposed in case you need direct access.
|
||||
// However, as a general rule, you should be able to do
|
||||
// most of what you need using only this class.
|
||||
final KeyboardService service;
|
||||
|
||||
KeyboardHandle _currentHandle;
|
||||
|
||||
KeyboardHandle show(KeyboardClientStub stub) {
|
||||
assert(stub != null);
|
||||
if (_currentHandle != null) {
|
||||
if (_currentHandle.stub == stub)
|
||||
return _currentHandle;
|
||||
_currentHandle.release();
|
||||
}
|
||||
_currentHandle = new KeyboardHandle._show(this, stub);
|
||||
return _currentHandle;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class KeyboardHandle {
|
||||
|
||||
KeyboardHandle._show(Keyboard keyboard, this.stub) : _keyboard = keyboard {
|
||||
_keyboard.service.show(stub);
|
||||
_attached = true;
|
||||
}
|
||||
|
||||
KeyboardHandle._unattached(Keyboard keyboard) : _keyboard = keyboard, stub = null, _attached = false;
|
||||
static final unattached = new KeyboardHandle._unattached(keyboard);
|
||||
|
||||
final Keyboard _keyboard;
|
||||
final KeyboardClientStub stub;
|
||||
|
||||
bool _attached;
|
||||
bool get attached => _attached;
|
||||
|
||||
void showByRequest() {
|
||||
assert(_attached);
|
||||
assert(_keyboard._currentHandle == this);
|
||||
_keyboard.service.showByRequest();
|
||||
}
|
||||
|
||||
void release() {
|
||||
if (_attached) {
|
||||
assert(_keyboard._currentHandle == this);
|
||||
_keyboard.service.hide();
|
||||
_attached = false;
|
||||
_keyboard._currentHandle = null;
|
||||
}
|
||||
assert(_keyboard._currentHandle != this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final Keyboard keyboard = new Keyboard(_KeyboardConnection.instance.keyboardService);
|
||||
|
||||
56
sky/sdk/lib/widgets/focus.dart
Normal file
56
sky/sdk/lib/widgets/focus.dart
Normal file
@ -0,0 +1,56 @@
|
||||
// Copyright 2015 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:sky/widgets/widget.dart';
|
||||
|
||||
class Focus extends Inherited {
|
||||
|
||||
// TODO(ianh): This doesn't yet support nested scopes. We should not
|
||||
// be telling our _currentlyFocusedKey that they are focused if we
|
||||
// ourselves are not focused. Otherwise if you have a dialog with a
|
||||
// text field over the top of a pane with a text field, they'll
|
||||
// fight over control of the keyboard.
|
||||
|
||||
Focus({
|
||||
GlobalKey key,
|
||||
GlobalKey initialFocus,
|
||||
Widget child
|
||||
}) : super(key: key, child: child);
|
||||
|
||||
GlobalKey initialFocus;
|
||||
|
||||
GlobalKey _currentlyFocusedKey;
|
||||
GlobalKey get currentlyFocusedKey {
|
||||
if (_currentlyFocusedKey != null)
|
||||
return _currentlyFocusedKey;
|
||||
return initialFocus;
|
||||
}
|
||||
void set currentlyFocusedKey(GlobalKey value) {
|
||||
if (value != _currentlyFocusedKey) {
|
||||
_currentlyFocusedKey = value;
|
||||
notifyDescendants();
|
||||
}
|
||||
}
|
||||
|
||||
void syncState(Focus old) {
|
||||
_currentlyFocusedKey = old._currentlyFocusedKey;
|
||||
super.syncState(old);
|
||||
}
|
||||
|
||||
static bool at(Component component) {
|
||||
assert(component != null);
|
||||
assert(component.key is GlobalKey);
|
||||
Focus focus = component.inheritedOfType(Focus);
|
||||
return focus == null || focus.currentlyFocusedKey == component.key;
|
||||
}
|
||||
|
||||
static void moveTo(Component component) {
|
||||
assert(component != null);
|
||||
assert(component.key is GlobalKey);
|
||||
Focus focus = component.inheritedOfType(Focus);
|
||||
if (focus != null)
|
||||
focus.currentlyFocusedKey = component.key;
|
||||
}
|
||||
|
||||
}
|
||||
@ -21,10 +21,7 @@ final bool _shouldLogRenderDuration = false;
|
||||
typedef Widget Builder();
|
||||
typedef void WidgetTreeWalker(Widget);
|
||||
|
||||
abstract class KeyBase {
|
||||
}
|
||||
|
||||
abstract class Key extends KeyBase {
|
||||
abstract class Key {
|
||||
Key.constructor(); // so that subclasses can call us, since the Key() factory constructor shadows the implicit constructor
|
||||
factory Key(String value) => new StringKey(value);
|
||||
factory Key.stringify(Object value) => new StringKey(value.toString());
|
||||
@ -43,7 +40,7 @@ class StringKey extends Key {
|
||||
class ObjectKey extends Key {
|
||||
ObjectKey(this.value) : super.constructor();
|
||||
final Object value;
|
||||
String toString() => '[Instance of ${value.runtimeType}]';
|
||||
String toString() => '[${value.runtimeType}(${value.hashCode})]';
|
||||
bool operator==(other) => other is ObjectKey && identical(other.value, value);
|
||||
int get hashCode => identityHashCode(value);
|
||||
}
|
||||
@ -53,6 +50,11 @@ class UniqueKey extends Key {
|
||||
String toString() => '[$hashCode]';
|
||||
}
|
||||
|
||||
class GlobalKey extends Key {
|
||||
GlobalKey() : super.constructor();
|
||||
String toString() => '[Global Key $hashCode]';
|
||||
}
|
||||
|
||||
/// A base class for elements of the widget tree
|
||||
abstract class Widget {
|
||||
|
||||
@ -326,22 +328,29 @@ abstract class Inherited extends TagNode {
|
||||
Inherited({ Key key, Widget child }) : super._withKey(child, key);
|
||||
|
||||
void _sync(Widget old, dynamic slot) {
|
||||
if (old != null && syncShouldNotify(old)) {
|
||||
final Type ourRuntimeType = runtimeType;
|
||||
void notifyChildren(Widget child) {
|
||||
if (child is Component &&
|
||||
child._dependencies != null &&
|
||||
child._dependencies.contains(ourRuntimeType))
|
||||
child._dependenciesChanged();
|
||||
if (child.runtimeType != ourRuntimeType)
|
||||
child.walkChildren(notifyChildren);
|
||||
}
|
||||
walkChildren(notifyChildren);
|
||||
if (old != null) {
|
||||
syncState(old);
|
||||
if (syncShouldNotify(old))
|
||||
notifyDescendants();
|
||||
}
|
||||
super._sync(old, slot);
|
||||
}
|
||||
|
||||
bool syncShouldNotify(Inherited old);
|
||||
void notifyDescendants() {
|
||||
final Type ourRuntimeType = runtimeType;
|
||||
void notifyChildren(Widget child) {
|
||||
if (child is Component &&
|
||||
child._dependencies != null &&
|
||||
child._dependencies.contains(ourRuntimeType))
|
||||
child._dependenciesChanged();
|
||||
if (child.runtimeType != ourRuntimeType)
|
||||
child.walkChildren(notifyChildren);
|
||||
}
|
||||
walkChildren(notifyChildren);
|
||||
}
|
||||
|
||||
void syncState(Inherited old) { }
|
||||
bool syncShouldNotify(Inherited old) => false;
|
||||
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user