mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Improve Material ink effects
1) Factors InkWell out of Material so that components can use an ink well without needing the shadow/level machinery. 2) Makes the ink effect move at a different velocity once the tap has actually occurred, converging with the spec. We don't have the right speeds yet, but at least we're approaching the right shape. 3) To support (2), added a primaryPointer attribute to GestureEvents to let authors coorelate gesturetapdown events with later gesturetap events. 4) To support (2), modernized SplashAnimation to used AnimatedValue and friends. 5) Added more constants to view-configuration.dart that match Android. I've also removed the cancelling of the ink effect on scroll. The proper way to do that is to notice that someone in the event chain is listening for scrollstart and delay the beginning of the ink effect for some period of time. R=eseidel@chromium.org Review URL: https://codereview.chromium.org/1019023003
This commit is contained in:
parent
1391f06fb1
commit
97abc0dadd
@ -67,6 +67,7 @@ GestureEvent::GestureEvent()
|
||||
|
||||
GestureEvent::GestureEvent(const WebGestureEvent& event)
|
||||
: Event(stringForType(event.type), true, true)
|
||||
, m_primaryPointer(event.primaryPointer)
|
||||
, m_x(event.x)
|
||||
, m_y(event.y)
|
||||
, m_dx(0)
|
||||
@ -90,6 +91,7 @@ GestureEvent::GestureEvent(const WebGestureEvent& event)
|
||||
|
||||
GestureEvent::GestureEvent(const AtomicString& type, const GestureEventInit& initializer)
|
||||
: Event(type, initializer)
|
||||
, m_primaryPointer(initializer.primaryPointer)
|
||||
, m_x(initializer.x)
|
||||
, m_y(initializer.y)
|
||||
, m_dx(initializer.dx)
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
namespace blink {
|
||||
|
||||
struct GestureEventInit : public EventInit {
|
||||
int primaryPointer = 0;
|
||||
double x = 0;
|
||||
double y = 0;
|
||||
double dx = 0;
|
||||
@ -38,6 +39,7 @@ public:
|
||||
~GestureEvent() override;
|
||||
const AtomicString& interfaceName() const override;
|
||||
|
||||
int primaryPointer() const { return m_primaryPointer; }
|
||||
float x() const { return m_x; }
|
||||
float y() const { return m_y; }
|
||||
float dx() const { return m_dx; }
|
||||
@ -50,6 +52,7 @@ private:
|
||||
explicit GestureEvent(const WebGestureEvent&);
|
||||
GestureEvent(const AtomicString& type, const GestureEventInit&);
|
||||
|
||||
int m_primaryPointer;
|
||||
float m_x;
|
||||
float m_y;
|
||||
float m_dx;
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
[
|
||||
EventConstructor,
|
||||
] interface GestureEvent : Event {
|
||||
[InitializedByEventConstructor] readonly attribute long primaryPointer;
|
||||
[InitializedByEventConstructor] readonly attribute double x;
|
||||
[InitializedByEventConstructor] readonly attribute double y;
|
||||
[InitializedByEventConstructor] readonly attribute double dx;
|
||||
|
||||
@ -268,6 +268,7 @@ public:
|
||||
|
||||
class WebGestureEvent : public WebInputEvent {
|
||||
public:
|
||||
int primaryPointer = 0;
|
||||
float x = 0;
|
||||
float y = 0;
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:sky/framework/components/material.dart';
|
||||
import 'package:sky/framework/components/ink_well.dart';
|
||||
import 'package:sky/framework/fn.dart';
|
||||
import 'package:sky/framework/theme/typography.dart' as typography;
|
||||
import 'stock_arrow.dart';
|
||||
@ -68,7 +68,7 @@ class StockRow extends Component {
|
||||
)
|
||||
];
|
||||
|
||||
return new Material(
|
||||
return new InkWell(
|
||||
style: _style,
|
||||
children: children
|
||||
);
|
||||
|
||||
@ -2,45 +2,69 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import '../animation/animated_value.dart';
|
||||
import '../animation/curves.dart';
|
||||
import '../animation/generators.dart';
|
||||
import '../fn.dart';
|
||||
import '../theme/view-configuration.dart' as config;
|
||||
import 'dart:async';
|
||||
import 'dart:math' as math;
|
||||
import 'dart:sky' as sky;
|
||||
|
||||
const double _kSplashSize = 400.0;
|
||||
const double _kSplashDuration = 500.0;
|
||||
const double _kSplashConfirmedDuration = 350.0;
|
||||
const double _kSplashUnconfirmedDuration = config.kDefaultLongPressTimeout;
|
||||
|
||||
class SplashAnimation {
|
||||
AnimationGenerator _animation;
|
||||
double _getSplashTargetSize(sky.ClientRect rect, double x, double y) {
|
||||
return 2.0 * math.max(math.max(x - rect.left, rect.right - x),
|
||||
math.max(y - rect.top, rect.bottom - y));
|
||||
}
|
||||
|
||||
class SplashController {
|
||||
final int pointer;
|
||||
Stream<String> get onStyleChanged => _styleStream;
|
||||
|
||||
final AnimatedValue _size = new AnimatedValue(0.0);
|
||||
double _offsetX;
|
||||
double _offsetY;
|
||||
double _targetSize;
|
||||
Stream<String> _styleStream;
|
||||
|
||||
Stream<String> _styleChanged;
|
||||
void start() {
|
||||
_size.animateTo(_targetSize, _kSplashUnconfirmedDuration, curve: easeOut);
|
||||
}
|
||||
|
||||
Stream<String> get onStyleChanged => _styleChanged;
|
||||
void confirm() {
|
||||
double fractionRemaining = (_targetSize - _size.value) / _targetSize;
|
||||
double duration = fractionRemaining * _kSplashConfirmedDuration;
|
||||
if (duration <= 0.0)
|
||||
return;
|
||||
_size.animateTo(_targetSize, duration, curve: easeOut);
|
||||
}
|
||||
|
||||
void cancel() => _animation.cancel();
|
||||
void cancel() {
|
||||
_size.stop();
|
||||
}
|
||||
|
||||
SplashAnimation(sky.ClientRect rect, double x, double y,
|
||||
{ Function onDone })
|
||||
SplashController(sky.ClientRect rect, double x, double y,
|
||||
{ this.pointer, Function onDone })
|
||||
: _offsetX = x - rect.left,
|
||||
_offsetY = y - rect.top {
|
||||
_offsetY = y - rect.top,
|
||||
_targetSize = _getSplashTargetSize(rect, x, y) {
|
||||
|
||||
_animation = new AnimationGenerator(
|
||||
duration:_kSplashDuration,
|
||||
end: _kSplashSize,
|
||||
curve: easeOut,
|
||||
onDone: onDone);
|
||||
_styleStream = _size.onValueChanged.map((p) {
|
||||
if (p == _targetSize) {
|
||||
onDone();
|
||||
}
|
||||
return '''
|
||||
top: ${_offsetY - p/2}px;
|
||||
left: ${_offsetX - p/2}px;
|
||||
width: ${p}px;
|
||||
height: ${p}px;
|
||||
border-radius: ${p}px;
|
||||
opacity: ${1.0 - (p / _targetSize)};''';
|
||||
});
|
||||
|
||||
_styleChanged = _animation.onTick.map((p) => '''
|
||||
top: ${_offsetY - p/2}px;
|
||||
left: ${_offsetX - p/2}px;
|
||||
width: ${p}px;
|
||||
height: ${p}px;
|
||||
border-radius: ${p}px;
|
||||
opacity: ${1.0 - (p / _kSplashSize)};
|
||||
''');
|
||||
start();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
85
framework/components/ink_well.dart
Normal file
85
framework/components/ink_well.dart
Normal file
@ -0,0 +1,85 @@
|
||||
// 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 '../fn.dart';
|
||||
import 'dart:collection';
|
||||
import 'dart:sky' as sky;
|
||||
import 'ink_splash.dart';
|
||||
|
||||
class InkWell extends Component {
|
||||
LinkedHashSet<SplashController> _splashes;
|
||||
|
||||
Style style;
|
||||
String inlineStyle;
|
||||
List<Node> children;
|
||||
|
||||
InkWell({ Object key, this.style, this.inlineStyle, this.children })
|
||||
: super(key: key);
|
||||
|
||||
Node build() {
|
||||
List<Node> childrenIncludingSplashes = [];
|
||||
|
||||
if (_splashes != null) {
|
||||
childrenIncludingSplashes.addAll(
|
||||
_splashes.map((s) => new InkSplash(s.onStyleChanged)));
|
||||
}
|
||||
|
||||
if (children != null)
|
||||
childrenIncludingSplashes.addAll(children);
|
||||
|
||||
return new EventTarget(
|
||||
new Container(
|
||||
style: style,
|
||||
inlineStyle: inlineStyle,
|
||||
children: childrenIncludingSplashes),
|
||||
onGestureTapDown: _startSplash,
|
||||
onGestureTap: _confirmSplash
|
||||
);
|
||||
}
|
||||
|
||||
sky.ClientRect _getBoundingRect() => (getRoot() as sky.Element).getBoundingClientRect();
|
||||
|
||||
void _startSplash(sky.GestureEvent event) {
|
||||
setState(() {
|
||||
if (_splashes == null)
|
||||
_splashes = new LinkedHashSet<SplashController>();
|
||||
var splash;
|
||||
splash = new SplashController(_getBoundingRect(), event.x, event.y,
|
||||
pointer: event.primaryPointer,
|
||||
onDone: () { _splashDone(splash); });
|
||||
_splashes.add(splash);
|
||||
});
|
||||
}
|
||||
|
||||
void _confirmSplash(sky.GestureEvent event) {
|
||||
if (_splashes == null)
|
||||
return;
|
||||
_splashes.where((splash) => splash.pointer == event.primaryPointer)
|
||||
.forEach((splash) { splash.confirm(); });
|
||||
}
|
||||
|
||||
void _cancelSplashes(sky.Event event) {
|
||||
if (_splashes == null)
|
||||
return;
|
||||
setState(() {
|
||||
var splashes = _splashes;
|
||||
_splashes = null;
|
||||
splashes.forEach((s) { s.cancel(); });
|
||||
});
|
||||
}
|
||||
|
||||
void didUnmount() {
|
||||
_cancelSplashes(null);
|
||||
}
|
||||
|
||||
void _splashDone(SplashController splash) {
|
||||
if (_splashes == null)
|
||||
return;
|
||||
setState(() {
|
||||
_splashes.remove(splash);
|
||||
if (_splashes.length == 0)
|
||||
_splashes = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -4,9 +4,7 @@
|
||||
|
||||
import '../fn.dart';
|
||||
import '../theme/shadows.dart';
|
||||
import 'dart:collection';
|
||||
import 'dart:sky' as sky;
|
||||
import 'ink_splash.dart';
|
||||
import 'ink_well.dart';
|
||||
|
||||
class Material extends Component {
|
||||
static final List<Style> shadowStyle = [
|
||||
@ -18,8 +16,6 @@ class Material extends Component {
|
||||
new Style('box-shadow: ${Shadow[5]}'),
|
||||
];
|
||||
|
||||
LinkedHashSet<SplashAnimation> _splashes;
|
||||
|
||||
Style style;
|
||||
String inlineStyle;
|
||||
List<Node> children;
|
||||
@ -33,69 +29,10 @@ class Material extends Component {
|
||||
this.level: 0 }) : super(key: key);
|
||||
|
||||
Node build() {
|
||||
List<Node> childrenIncludingSplashes = [];
|
||||
|
||||
if (_splashes != null) {
|
||||
childrenIncludingSplashes.addAll(
|
||||
_splashes.map((s) => new InkSplash(s.onStyleChanged)));
|
||||
}
|
||||
|
||||
if (children != null)
|
||||
childrenIncludingSplashes.addAll(children);
|
||||
|
||||
return new EventTarget(
|
||||
new Container(
|
||||
style: level > 0 ? style.extend(shadowStyle[level]) : style,
|
||||
inlineStyle: inlineStyle,
|
||||
children: childrenIncludingSplashes),
|
||||
onGestureScrollStart: _cancelSplashes,
|
||||
onWheel: _cancelSplashes,
|
||||
onPointerDown: _startSplash
|
||||
return new InkWell(
|
||||
style: level > 0 ? style.extend(shadowStyle[level]) : style,
|
||||
inlineStyle: inlineStyle,
|
||||
children: children
|
||||
);
|
||||
}
|
||||
|
||||
sky.ClientRect _getBoundingRect() => (getRoot() as sky.Element).getBoundingClientRect();
|
||||
|
||||
void _startSplash(sky.PointerEvent event) {
|
||||
setState(() {
|
||||
if (_splashes == null) {
|
||||
_splashes = new LinkedHashSet<SplashAnimation>();
|
||||
}
|
||||
|
||||
var splash;
|
||||
splash = new SplashAnimation(_getBoundingRect(), event.x, event.y,
|
||||
onDone: () { _splashDone(splash); });
|
||||
|
||||
_splashes.add(splash);
|
||||
});
|
||||
}
|
||||
|
||||
void _cancelSplashes(sky.Event event) {
|
||||
if (_splashes == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
var splashes = _splashes;
|
||||
_splashes = null;
|
||||
splashes.forEach((s) { s.cancel(); });
|
||||
});
|
||||
}
|
||||
|
||||
void didUnmount() {
|
||||
_cancelSplashes(null);
|
||||
}
|
||||
|
||||
void _splashDone(SplashAnimation splash) {
|
||||
if (_splashes == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_splashes.remove(splash);
|
||||
if (_splashes.length == 0) {
|
||||
_splashes = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
import '../fn.dart';
|
||||
import 'button_base.dart';
|
||||
import 'icon.dart';
|
||||
import 'material.dart';
|
||||
import 'ink_well.dart';
|
||||
|
||||
class MenuItem extends ButtonBase {
|
||||
static final Style _style = new Style('''
|
||||
@ -40,7 +40,7 @@ class MenuItem extends ButtonBase {
|
||||
MenuItem({ Object key, this.icon, this.children }) : super(key: key);
|
||||
|
||||
Node buildContent() {
|
||||
return new Material (
|
||||
return new InkWell (
|
||||
style: highlight ? _highlightStyle : _style,
|
||||
children: [
|
||||
new Icon(
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
import '../animation/animated_value.dart';
|
||||
import '../fn.dart';
|
||||
import 'material.dart';
|
||||
import 'ink_well.dart';
|
||||
|
||||
class PopupMenuItem extends Component {
|
||||
static final Style _style = new Style('''
|
||||
@ -25,7 +25,7 @@ class PopupMenuItem extends Component {
|
||||
Node build() {
|
||||
_opacity.ensureListening();
|
||||
|
||||
return new Material(
|
||||
return new InkWell(
|
||||
style: _style,
|
||||
inlineStyle: _opacity.value == null ? null : 'opacity: ${_opacity.value}',
|
||||
children: children
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
|
||||
import '../fn.dart';
|
||||
import 'button_base.dart';
|
||||
import 'ink_well.dart';
|
||||
import 'material.dart';
|
||||
|
||||
typedef void ValueChanged(value);
|
||||
@ -54,7 +55,7 @@ class Radio extends ButtonBase {
|
||||
|
||||
Node buildContent() {
|
||||
return new EventTarget(
|
||||
new Material(
|
||||
new InkWell(
|
||||
style: highlight ? _highlightStyle : _style,
|
||||
children: value == groupValue ? [new Container(style: _dotStyle )] : []
|
||||
),
|
||||
|
||||
@ -175,6 +175,7 @@ class EventTarget extends Node {
|
||||
GestureEventListener onGestureScrollStart,
|
||||
GestureEventListener onGestureScrollUpdate,
|
||||
GestureEventListener onGestureTap,
|
||||
GestureEventListener onGestureTapDown,
|
||||
PointerEventListener onPointerCancel,
|
||||
PointerEventListener onPointerDown,
|
||||
PointerEventListener onPointerMove,
|
||||
@ -197,6 +198,8 @@ class EventTarget extends Node {
|
||||
listeners['gesturescrollupdate'] = onGestureScrollUpdate;
|
||||
if (onGestureTap != null)
|
||||
listeners['gesturetap'] = onGestureTap;
|
||||
if (onGestureTapDown != null)
|
||||
listeners['gesturetapdown'] = onGestureTapDown;
|
||||
if (onPointerCancel != null)
|
||||
listeners['pointercancel'] = onPointerCancel;
|
||||
if (onPointerDown != null)
|
||||
@ -216,6 +219,7 @@ class EventTarget extends Node {
|
||||
GestureEventListener onGestureScrollStart,
|
||||
GestureEventListener onGestureScrollUpdate,
|
||||
GestureEventListener onGestureTap,
|
||||
GestureEventListener onGestureTapDown,
|
||||
PointerEventListener onPointerCancel,
|
||||
PointerEventListener onPointerDown,
|
||||
PointerEventListener onPointerMove,
|
||||
@ -229,6 +233,7 @@ class EventTarget extends Node {
|
||||
onGestureScrollUpdate: onGestureScrollUpdate,
|
||||
onGestureScrollStart: onGestureScrollStart,
|
||||
onGestureTap: onGestureTap,
|
||||
onGestureTapDown: onGestureTapDown,
|
||||
onPointerCancel: onPointerCancel,
|
||||
onPointerDown: onPointerDown,
|
||||
onPointerMove: onPointerMove,
|
||||
|
||||
@ -5,9 +5,25 @@
|
||||
// Modeled after Android's ViewConfiguration:
|
||||
// https://github.com/android/platform_frameworks_base/blob/master/core/java/android/view/ViewConfiguration.java
|
||||
|
||||
const int kMaxFlingVelocity = 8000;
|
||||
const int kMinFlingVelocity = 50;
|
||||
const int kScrollbarFadeDelay = 300;
|
||||
const int kScrollbarFadeDuration = 250;
|
||||
const int kScrollbarSize = 10;
|
||||
const int kStatusBarHeight = 25;
|
||||
const double kStatusBarHeight = 25.0;
|
||||
const double kScrollbarSize = 10.0;
|
||||
const double kScrollbarFadeDuration = 250.0;
|
||||
const double kScrollbarFadeDelay = 300.0;
|
||||
const double kFadingEdgeLength = 12.0;
|
||||
const double kPressedStateDuration = 64.0;
|
||||
const double kDefaultLongPressTimeout = 500.0;
|
||||
const double kTapTimeout = 100.0;
|
||||
const double kJumpTapTimeout = 500.0;
|
||||
const double kDoubleTapTimeout = 300.0;
|
||||
const double kDoubleTapMinTime = 40.0;
|
||||
const double kHoverTapTimeout = 150.0;
|
||||
const double kHoverTapSlop = 20.0;
|
||||
const double kZoomControlsTimeout = 3000.0;
|
||||
const double kEdgeSlop = 12.0;
|
||||
const double kTouchSlop = 8.0;
|
||||
const double kDoubleTapTouchSlop = kTouchSlop;
|
||||
const double kPagingTouchSlop = kTouchSlop * 2.0;
|
||||
const double kDoubleTapSlop = 100.0;
|
||||
const double kWindowTouchSlop = 16.0;
|
||||
const double kMinFlingVelocity = 50.0;
|
||||
const double kMaxFlingVelocity = 8000.0;
|
||||
|
||||
@ -46,6 +46,7 @@ struct PointerData {
|
||||
};
|
||||
|
||||
struct GestureData {
|
||||
int32 primary_pointer;
|
||||
float x;
|
||||
float y;
|
||||
float dx;
|
||||
|
||||
@ -38,9 +38,11 @@ public class GestureProvider implements GestureDetector.OnGestureListener {
|
||||
}
|
||||
|
||||
private InputEvent createGestureEvent(MotionEvent event) {
|
||||
int pointerIndex = event.getActionIndex();
|
||||
GestureData gestureData = new GestureData();
|
||||
gestureData.x = event.getX();
|
||||
gestureData.y = event.getY();
|
||||
gestureData.primaryPointer = event.getPointerId(pointerIndex);
|
||||
gestureData.x = event.getX(pointerIndex);
|
||||
gestureData.y = event.getY(pointerIndex);
|
||||
InputEvent inputEvent = new InputEvent();
|
||||
inputEvent.timeStamp = event.getEventTime();
|
||||
inputEvent.gestureData = gestureData;
|
||||
|
||||
@ -96,6 +96,7 @@ scoped_ptr<blink::WebInputEvent> BuildWebGestureEvent(
|
||||
}
|
||||
|
||||
if (event->gesture_data) {
|
||||
web_event->primaryPointer = event->gesture_data->primary_pointer;
|
||||
web_event->x = event->gesture_data->x / device_pixel_ratio;
|
||||
web_event->y = event->gesture_data->y / device_pixel_ratio;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user