mirror of
https://github.com/dart-lang/sdk
synced 2024-10-04 16:54:55 +00:00
Apply dart fix to swarm sample
Change-Id: Ib111d262b0dc2572ce6cb773aed5547fe897b4dd Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/252520 Reviewed-by: Brian Wilkerson <brianwilkerson@google.com> Commit-Queue: Vijay Menon <vsm@google.com>
This commit is contained in:
parent
48693c6b3d
commit
a9092c76ac
|
@ -6,18 +6,16 @@
|
|||
|
||||
part of swarmlib;
|
||||
|
||||
/**
|
||||
* The base class that should be extended by all HTML applications.
|
||||
*
|
||||
* It should both be easy to use for users coming over from JavaScript, but
|
||||
* also offer a clear notion of OO encapsulation.
|
||||
*
|
||||
* This class or something similar belongs in the standard DOM library.
|
||||
*/
|
||||
/// The base class that should be extended by all HTML applications.
|
||||
///
|
||||
/// It should both be easy to use for users coming over from JavaScript, but
|
||||
/// also offer a clear notion of OO encapsulation.
|
||||
///
|
||||
/// This class or something similar belongs in the standard DOM library.
|
||||
class App {
|
||||
App() {}
|
||||
App();
|
||||
|
||||
/** Begins executing code in this [App]. */
|
||||
/// Begins executing code in this [App]. */
|
||||
void run() {
|
||||
// If the script is async, by the time we get here the DOM content may
|
||||
// already be loaded, so waiting on the DOMContentLoaded event is a no-op.
|
||||
|
@ -40,12 +38,10 @@ class App {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the DOM is fully loaded but potentially before resources.
|
||||
*
|
||||
* For most apps, any startup code should be in this method. Be sure to call
|
||||
* the superclass implementation.
|
||||
*/
|
||||
/// Called when the DOM is fully loaded but potentially before resources.
|
||||
///
|
||||
/// For most apps, any startup code should be in this method. Be sure to call
|
||||
/// the superclass implementation.
|
||||
void onLoad() {
|
||||
// Prevent the default browser behavior of scrolling the window.
|
||||
document.onTouchMove.listen((Event event) => event.preventDefault());
|
||||
|
@ -57,11 +53,9 @@ class App {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase the static splash screen.
|
||||
*
|
||||
* Assumption: if a splash screen exists, an element #appSplash contains it.
|
||||
*/
|
||||
/// Erase the static splash screen.
|
||||
///
|
||||
/// Assumption: if a splash screen exists, an element #appSplash contains it.
|
||||
void eraseSplashScreen() {
|
||||
final splash = document.querySelector("#appSplash");
|
||||
// Delete it if found, but it's okay for it not to be -- maybe
|
||||
|
@ -71,10 +65,8 @@ class App {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Swaps and reloads the app cache if an update is ready. Returns false if
|
||||
* an update is not ready.
|
||||
*/
|
||||
/// Swaps and reloads the app cache if an update is ready. Returns false if
|
||||
/// an update is not ready.
|
||||
bool swapAndReloadCache() {
|
||||
ApplicationCache appCache = window.applicationCache;
|
||||
if (!identical(appCache.status, ApplicationCache.UPDATEREADY)) {
|
||||
|
@ -88,7 +80,7 @@ class App {
|
|||
return true;
|
||||
}
|
||||
|
||||
/** Returns true if we are running as a packaged application. */
|
||||
/// Returns true if we are running as a packaged application. */
|
||||
static bool get isPackaged {
|
||||
return window.location.protocol == 'chrome-extension:';
|
||||
}
|
||||
|
|
|
@ -2,33 +2,25 @@
|
|||
|
||||
part of swarmlib;
|
||||
|
||||
/**
|
||||
* An iterator that allows the user to move forward and backward though
|
||||
* a set of items. (Bi-directional)
|
||||
*/
|
||||
/// An iterator that allows the user to move forward and backward though
|
||||
/// a set of items. (Bi-directional)
|
||||
class BiIterator<E> {
|
||||
/**
|
||||
* Provides forward and backward iterator functionality to keep track
|
||||
* which item is currently selected.
|
||||
*/
|
||||
/// Provides forward and backward iterator functionality to keep track
|
||||
/// which item is currently selected.
|
||||
ObservableValue<int> currentIndex;
|
||||
|
||||
/**
|
||||
* The collection of items we will be iterating through.
|
||||
*/
|
||||
/// The collection of items we will be iterating through.
|
||||
List<E> list;
|
||||
|
||||
BiIterator(this.list, [List<ChangeListener> oldListeners = null])
|
||||
: currentIndex = new ObservableValue<int>(0) {
|
||||
BiIterator(this.list, [List<ChangeListener> oldListeners])
|
||||
: currentIndex = ObservableValue<int>(0) {
|
||||
if (oldListeners != null) {
|
||||
currentIndex.listeners = oldListeners;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next section from the sections, given the current
|
||||
* position. Returns the last source if there is no next section.
|
||||
*/
|
||||
/// Returns the next section from the sections, given the current
|
||||
/// position. Returns the last source if there is no next section.
|
||||
E next() {
|
||||
if (currentIndex.value < list.length - 1) {
|
||||
currentIndex.value += 1;
|
||||
|
@ -36,19 +28,15 @@ class BiIterator<E> {
|
|||
return list[currentIndex.value];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current Section (page in the UI) that the user is
|
||||
* looking at.
|
||||
*/
|
||||
/// Returns the current Section (page in the UI) that the user is
|
||||
/// looking at.
|
||||
E get current {
|
||||
return list[currentIndex.value];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the previous section from the sections, given the current
|
||||
* position. Returns the front section if we are already at the front of
|
||||
* the list.
|
||||
*/
|
||||
/// Returns the previous section from the sections, given the current
|
||||
/// position. Returns the front section if we are already at the front of
|
||||
/// the list.
|
||||
E previous() {
|
||||
if (currentIndex.value > 0) {
|
||||
currentIndex.value -= 1;
|
||||
|
@ -56,9 +44,7 @@ class BiIterator<E> {
|
|||
return list[currentIndex.value];
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the iterator pointer over so that it points to a given list item.
|
||||
*/
|
||||
/// Move the iterator pointer over so that it points to a given list item.
|
||||
void jumpToValue(E val) {
|
||||
for (int i = 0; i < list.length; i++) {
|
||||
if (identical(list[i], val)) {
|
||||
|
|
|
@ -8,7 +8,7 @@ part of swarmlib;
|
|||
|
||||
// TODO(jimhug): Work out correct copyright for this file.
|
||||
class CannedData {
|
||||
static const Map<String, String> data = const {
|
||||
static const Map<String, String> data = {
|
||||
'Test0_0_0.html': 'Lorem ipsum something or other...',
|
||||
'Test0_0_1.html': 'Lorem ipsum something or other...',
|
||||
'Test0_0_2.html': 'Lorem ipsum something or other...',
|
||||
|
|
|
@ -14,18 +14,19 @@ class ConfigHintDialog extends DialogView {
|
|||
|
||||
factory ConfigHintDialog(CompositeView parent, Function doneHandler) {
|
||||
View content = ConfigHintDialog.makeContent();
|
||||
return new ConfigHintDialog._impl(parent, doneHandler, content);
|
||||
return ConfigHintDialog._impl(parent, doneHandler, content);
|
||||
}
|
||||
|
||||
ConfigHintDialog._impl(this._parent, this._doneHandler, View content)
|
||||
: super('Feed configuration', '', content);
|
||||
|
||||
@override
|
||||
void onDone() {
|
||||
_doneHandler();
|
||||
}
|
||||
|
||||
static View makeContent() {
|
||||
return new View.html('''
|
||||
return View.html('''
|
||||
<div>
|
||||
Add or remove feeds in
|
||||
<a href="https://www.google.com/reader" target="_blank">
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
part of swarmlib;
|
||||
|
||||
/** The top-level collection of all sections for a user. */
|
||||
/// The top-level collection of all sections for a user. */
|
||||
// TODO(jimhug): This is known as UserData in the server model.
|
||||
class Sections extends IterableBase<Section> {
|
||||
final List<Section> _sections;
|
||||
|
@ -16,6 +16,7 @@ class Sections extends IterableBase<Section> {
|
|||
|
||||
operator [](int i) => _sections[i];
|
||||
|
||||
@override
|
||||
int get length => _sections.length;
|
||||
|
||||
List<String> get sectionTitles => _sections.map((s) => s.title).toList();
|
||||
|
@ -24,15 +25,14 @@ class Sections extends IterableBase<Section> {
|
|||
// TODO(jimhug): http://b/issue?id=5351067
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the Section object that has a given title.
|
||||
* This is used to integrate well with [ConveyorView].
|
||||
*/
|
||||
/// Find the Section object that has a given title.
|
||||
/// This is used to integrate well with [ConveyorView].
|
||||
Section findSection(String name) {
|
||||
return CollectionUtils.find(_sections, (sect) => sect.title == name);
|
||||
}
|
||||
|
||||
// TODO(jimhug): Track down callers!
|
||||
@override
|
||||
Iterator<Section> get iterator => _sections.iterator;
|
||||
|
||||
// TODO(jimhug): Better support for switching between local dev and server.
|
||||
|
@ -47,19 +47,19 @@ class Sections extends IterableBase<Section> {
|
|||
}
|
||||
|
||||
// This method is exposed for tests.
|
||||
static void initializeFromData(String data, void callback(Sections sects)) {
|
||||
final decoder = new Decoder(data);
|
||||
static void initializeFromData(String data, void Function(Sections sects) callback) {
|
||||
final decoder = Decoder(data);
|
||||
int nSections = decoder.readInt();
|
||||
final sections = new List<Section>();
|
||||
final sections = <Section>[];
|
||||
|
||||
for (int i = 0; i < nSections; i++) {
|
||||
sections.add(Section.decode(decoder));
|
||||
}
|
||||
callback(new Sections(sections));
|
||||
callback(Sections(sections));
|
||||
}
|
||||
|
||||
static void initializeFromUrl(
|
||||
bool useCannedData, void callback(Sections sections)) {
|
||||
bool useCannedData, void Function(Sections sections) callback) {
|
||||
if (Sections.runningFromFile || useCannedData) {
|
||||
initializeFromData(CannedData.data['user.data'], callback);
|
||||
} else {
|
||||
|
@ -79,9 +79,7 @@ class Sections extends IterableBase<Section> {
|
|||
return CollectionUtils.find(_sections, (section) => section.id == id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the name of a section, find its index in the set.
|
||||
*/
|
||||
/// Given the name of a section, find its index in the set.
|
||||
int findSectionIndex(String name) {
|
||||
for (int i = 0; i < _sections.length; i++) {
|
||||
if (name == _sections[i].title) {
|
||||
|
@ -94,10 +92,11 @@ class Sections extends IterableBase<Section> {
|
|||
List<Section> get sections => _sections;
|
||||
|
||||
// TODO(jmesserly): this should be a property
|
||||
@override
|
||||
bool get isEmpty => length == 0;
|
||||
}
|
||||
|
||||
/** A collection of data sources representing a page in the UI. */
|
||||
/// A collection of data sources representing a page in the UI. */
|
||||
class Section {
|
||||
final String id;
|
||||
final String title;
|
||||
|
@ -117,11 +116,11 @@ class Section {
|
|||
final sectionTitle = decoder.readString();
|
||||
|
||||
final nSources = decoder.readInt();
|
||||
final feeds = new ObservableList<Feed>();
|
||||
final feeds = ObservableList<Feed>();
|
||||
for (int j = 0; j < nSources; j++) {
|
||||
feeds.add(Feed.decode(decoder));
|
||||
}
|
||||
return new Section(sectionId, sectionTitle, feeds);
|
||||
return Section(sectionId, sectionTitle, feeds);
|
||||
}
|
||||
|
||||
Feed findFeed(String id_) {
|
||||
|
@ -129,7 +128,7 @@ class Section {
|
|||
}
|
||||
}
|
||||
|
||||
/** Provider of a news feed. */
|
||||
/// Provider of a news feed. */
|
||||
class Feed {
|
||||
String id;
|
||||
final String title;
|
||||
|
@ -138,15 +137,15 @@ class Feed {
|
|||
ObservableList<Article> articles;
|
||||
ObservableValue<bool> error; // TODO(jimhug): Check if dead code.
|
||||
|
||||
Feed(this.id, this.title, this.iconUrl, {this.description: ''})
|
||||
: articles = new ObservableList<Article>(),
|
||||
error = new ObservableValue<bool>(false);
|
||||
Feed(this.id, this.title, this.iconUrl, {this.description = ''})
|
||||
: articles = ObservableList<Article>(),
|
||||
error = ObservableValue<bool>(false);
|
||||
|
||||
static Feed decode(Decoder decoder) {
|
||||
final sourceId = decoder.readString();
|
||||
final sourceTitle = decoder.readString();
|
||||
final sourceIcon = decoder.readString();
|
||||
final feed = new Feed(sourceId, sourceTitle, sourceIcon);
|
||||
final feed = Feed(sourceId, sourceTitle, sourceIcon);
|
||||
final nItems = decoder.readInt();
|
||||
|
||||
for (int i = 0; i < nItems; i++) {
|
||||
|
@ -162,7 +161,7 @@ class Feed {
|
|||
void refresh() {}
|
||||
}
|
||||
|
||||
/** A single article or posting to display. */
|
||||
/// A single article or posting to display. */
|
||||
class Article {
|
||||
final String id;
|
||||
DateTime date;
|
||||
|
@ -179,9 +178,9 @@ class Article {
|
|||
|
||||
Article(this.dataSource, this.id, this.date, this.title, this.author,
|
||||
this.srcUrl, this.hasThumbnail, this.textBody,
|
||||
{htmlBody: null, bool unread: true, this.error: false})
|
||||
: unread = new ObservableValue<bool>(unread),
|
||||
this._htmlBody = htmlBody;
|
||||
{htmlBody, bool unread = true, this.error = false})
|
||||
: unread = ObservableValue<bool>(unread),
|
||||
_htmlBody = htmlBody;
|
||||
|
||||
String get htmlBody {
|
||||
_ensureLoaded();
|
||||
|
@ -198,7 +197,7 @@ class Article {
|
|||
String get thumbUrl {
|
||||
if (!hasThumbnail) return null;
|
||||
|
||||
var home;
|
||||
String home;
|
||||
if (Sections.runningFromFile) {
|
||||
home = 'http://dart.googleplex.com';
|
||||
} else {
|
||||
|
@ -221,7 +220,7 @@ class Article {
|
|||
_htmlBody = CannedData.data[name];
|
||||
} else {
|
||||
// TODO(jimhug): Remove this truly evil synchronoush xhr.
|
||||
final req = new HttpRequest();
|
||||
final req = HttpRequest();
|
||||
req.open('GET', 'data/$name', async: false);
|
||||
req.send();
|
||||
_htmlBody = req.responseText;
|
||||
|
@ -236,9 +235,9 @@ class Article {
|
|||
final author = decoder.readString();
|
||||
final dateInSeconds = decoder.readInt();
|
||||
final snippet = decoder.readString();
|
||||
final date = new DateTime.fromMillisecondsSinceEpoch(dateInSeconds * 1000,
|
||||
final date = DateTime.fromMillisecondsSinceEpoch(dateInSeconds * 1000,
|
||||
isUtc: true);
|
||||
return new Article(
|
||||
return Article(
|
||||
source, id, date, title, author, srcUrl, hasThumbnail, snippet);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ class Decoder {
|
|||
String data;
|
||||
|
||||
Decoder(this.data) {
|
||||
this.index = 0;
|
||||
index = 0;
|
||||
}
|
||||
|
||||
// Reads numbers in variable-length 7-bit encoding. This matches the
|
||||
|
|
|
@ -6,25 +6,24 @@
|
|||
|
||||
part of swarmlib;
|
||||
|
||||
/**
|
||||
* An informational dialog that shows keyboard shortcuts and provides a
|
||||
* link to the Dart language webpage.
|
||||
*/
|
||||
/// An informational dialog that shows keyboard shortcuts and provides a
|
||||
/// link to the Dart language webpage.
|
||||
//TODO(efortuna): fix DialogView so it doesn't require the HTML passed to
|
||||
// the constructor.
|
||||
class HelpDialog extends DialogView {
|
||||
CompositeView _parent;
|
||||
Function _doneHandler;
|
||||
final CompositeView _parent;
|
||||
final Function _doneHandler;
|
||||
|
||||
HelpDialog(this._parent, this._doneHandler)
|
||||
: super('Information', '', makeContent());
|
||||
|
||||
@override
|
||||
void onDone() {
|
||||
_doneHandler();
|
||||
}
|
||||
|
||||
static View makeContent() {
|
||||
return new View.html('''
|
||||
return View.html('''
|
||||
<div>
|
||||
|
||||
<p>
|
||||
|
@ -46,40 +45,40 @@ class HelpDialog extends DialogView {
|
|||
String cellStart = '''<th valign="middle" align="center">''';
|
||||
return '''<table width="90%" border=1 cellspacing="0" cellpadding="2">
|
||||
<tr bgcolor="#c3d9ff">
|
||||
${cellStart} Shortcut Key </th>
|
||||
${cellStart} Action </th>
|
||||
$cellStart Shortcut Key </th>
|
||||
$cellStart Action </th>
|
||||
</tr>
|
||||
<tr>
|
||||
${cellStart} j, <down arrow> </th>
|
||||
${cellStart} Next Article </th>
|
||||
$cellStart j, <down arrow> </th>
|
||||
$cellStart Next Article </th>
|
||||
</tr>
|
||||
<tr>
|
||||
${cellStart} k, <up arrow> </th>
|
||||
${cellStart} Previous Article </th>
|
||||
$cellStart k, <up arrow> </th>
|
||||
$cellStart Previous Article </th>
|
||||
</tr>
|
||||
<tr>
|
||||
${cellStart} o, <enter> </th>
|
||||
${cellStart} Open Article </th>
|
||||
$cellStart o, <enter> </th>
|
||||
$cellStart Open Article </th>
|
||||
</tr>
|
||||
<tr>
|
||||
${cellStart} <esc>, <delete> </th>
|
||||
${cellStart} Back </th>
|
||||
$cellStart <esc>, <delete> </th>
|
||||
$cellStart Back </th>
|
||||
</tr>
|
||||
<tr>
|
||||
${cellStart} a, h, <left arrow> </th>
|
||||
${cellStart} Left </th>
|
||||
$cellStart a, h, <left arrow> </th>
|
||||
$cellStart Left </th>
|
||||
</tr>
|
||||
<tr>
|
||||
${cellStart} d, l, <right arrow> </th>
|
||||
${cellStart} Right </th>
|
||||
$cellStart d, l, <right arrow> </th>
|
||||
$cellStart Right </th>
|
||||
</tr>
|
||||
<tr>
|
||||
${cellStart} n </th>
|
||||
${cellStart} Next Category </th>
|
||||
$cellStart n </th>
|
||||
$cellStart Next Category </th>
|
||||
</tr>
|
||||
<tr>
|
||||
${cellStart} p </th>
|
||||
${cellStart} Previous Category </th>
|
||||
$cellStart p </th>
|
||||
$cellStart Previous Category </th>
|
||||
</tr>
|
||||
|
||||
</table>''';
|
||||
|
|
|
@ -6,29 +6,25 @@
|
|||
|
||||
part of swarmlib;
|
||||
|
||||
/**
|
||||
* A simple news reader in Dart.
|
||||
*/
|
||||
/// A simple news reader in Dart.
|
||||
class Swarm extends App {
|
||||
/**
|
||||
* Flag to insure the onLoad isn't called when callback from initializeFromUrl
|
||||
* could occur before the document's onload event.
|
||||
*/
|
||||
/// Flag to insure the onLoad isn't called when callback from initializeFromUrl
|
||||
/// could occur before the document's onload event.
|
||||
bool onLoadFired;
|
||||
|
||||
/** Collections of datafeeds to show per page. */
|
||||
/// Collections of datafeeds to show per page. */
|
||||
Sections sections;
|
||||
|
||||
/** The front page of the app. */
|
||||
/// The front page of the app. */
|
||||
FrontView frontView;
|
||||
|
||||
/** Observable UI state. */
|
||||
/// Observable UI state. */
|
||||
SwarmState state;
|
||||
|
||||
Swarm({bool useCannedData: false}) : onLoadFired = false {
|
||||
Swarm({bool useCannedData = false}) : onLoadFired = false {
|
||||
Sections.initializeFromUrl(useCannedData, (currSections) {
|
||||
sections = currSections;
|
||||
state = new SwarmState(sections);
|
||||
state = SwarmState(sections);
|
||||
setupApp();
|
||||
});
|
||||
// Catch user keypresses and decide whether to use them for the
|
||||
|
@ -40,16 +36,14 @@ class Swarm extends App {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells each data source to check the server for the latest data.
|
||||
*/
|
||||
/// Tells each data source to check the server for the latest data.
|
||||
void refresh() {
|
||||
sections.refresh();
|
||||
|
||||
// Hook up listeners about any data source additions or deletions. We don't
|
||||
// differentiate additions or deletions just the fact that data feeds have
|
||||
// changed. We might want more fidelity later.
|
||||
sections.sectionTitles.forEach((title) {
|
||||
for (var title in sections.sectionTitles) {
|
||||
Section section = sections.findSection(title);
|
||||
// TODO(terry): addChangeListener needs to return an id so previous
|
||||
// listener can be removed, otherwise anonymous functions
|
||||
|
@ -58,19 +52,18 @@ class Swarm extends App {
|
|||
// TODO(jacobr): implement this.
|
||||
print("Refresh sections not impl yet.");
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** The page load event handler. */
|
||||
/// The page load event handler. */
|
||||
@override
|
||||
void onLoad() {
|
||||
onLoadFired = true;
|
||||
super.onLoad();
|
||||
setupApp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the application's world.
|
||||
*/
|
||||
/// Setup the application's world.
|
||||
void setupApp() {
|
||||
// TODO(terry): Should be able to spinup the app w/o waiting for data.
|
||||
// If the document is already loaded so we can setup the app anytime.
|
||||
|
@ -85,7 +78,7 @@ class Swarm extends App {
|
|||
}
|
||||
|
||||
void render() {
|
||||
frontView = new FrontView(this);
|
||||
frontView = FrontView(this);
|
||||
frontView.addToDocument(document.body);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,79 +6,62 @@
|
|||
|
||||
part of swarmlib;
|
||||
|
||||
/**
|
||||
* The top-level class for the UI state. UI state is essentially a "model" from
|
||||
* the view's perspective but whose data just describes the UI itself. It
|
||||
* contains data like the currently selected story, etc.
|
||||
*/
|
||||
/// The top-level class for the UI state. UI state is essentially a "model" from
|
||||
/// the view's perspective but whose data just describes the UI itself. It
|
||||
/// contains data like the currently selected story, etc.
|
||||
// TODO(jimhug): Split the two classes here into framework and app-specific.
|
||||
class SwarmState extends UIState {
|
||||
/** Core data source for the app. */
|
||||
/// Core data source for the app. */
|
||||
final Sections _dataModel;
|
||||
|
||||
/**
|
||||
* Which article the user is currently viewing, or null if they aren't
|
||||
* viewing an Article.
|
||||
*/
|
||||
/// Which article the user is currently viewing, or null if they aren't
|
||||
/// viewing an Article.
|
||||
final ObservableValue<Article> currentArticle;
|
||||
/**
|
||||
* Which article the user currently has selected (for traversing articles
|
||||
* via keyboard shortcuts).
|
||||
*/
|
||||
|
||||
/// Which article the user currently has selected (for traversing articles
|
||||
/// via keyboard shortcuts).
|
||||
final ObservableValue<Article> selectedArticle;
|
||||
|
||||
/**
|
||||
* True if the story view is maximized and the top and bottom UI elements
|
||||
* are hidden.
|
||||
*/
|
||||
/// True if the story view is maximized and the top and bottom UI elements
|
||||
/// are hidden.
|
||||
final ObservableValue<bool> storyMaximized;
|
||||
|
||||
/**
|
||||
* True if the maximized story, if any, is being displayed in text mode
|
||||
* rather than as an embedded web-page.
|
||||
*/
|
||||
/// True if the maximized story, if any, is being displayed in text mode
|
||||
/// rather than as an embedded web-page.
|
||||
final ObservableValue<bool> storyTextMode;
|
||||
|
||||
/**
|
||||
* Which article the user currently has selected (by keyboard shortcuts),
|
||||
* or null if an article isn't selected by the keyboard.
|
||||
*/
|
||||
/// Which article the user currently has selected (by keyboard shortcuts),
|
||||
/// or null if an article isn't selected by the keyboard.
|
||||
BiIterator<Article> _articleIterator;
|
||||
|
||||
/**
|
||||
* Which feed is currently selected (for keyboard shortcuts).
|
||||
*/
|
||||
/// Which feed is currently selected (for keyboard shortcuts).
|
||||
BiIterator<Feed> _feedIterator;
|
||||
|
||||
/**
|
||||
* Which section is currently selected (for keyboard shortcuts).
|
||||
*/
|
||||
/// Which section is currently selected (for keyboard shortcuts).
|
||||
BiIterator<Section> _sectionIterator;
|
||||
|
||||
SwarmState(this._dataModel)
|
||||
: currentArticle = new ObservableValue<Article>(null),
|
||||
selectedArticle = new ObservableValue<Article>(null),
|
||||
storyMaximized = new ObservableValue<bool>(false),
|
||||
storyTextMode = new ObservableValue<bool>(true) {
|
||||
: currentArticle = ObservableValue<Article>(null),
|
||||
selectedArticle = ObservableValue<Article>(null),
|
||||
storyMaximized = ObservableValue<bool>(false),
|
||||
storyTextMode = ObservableValue<bool>(true) {
|
||||
startHistoryTracking();
|
||||
// TODO(efortuna): consider having this class just hold observable
|
||||
// currentIndecies instead of iterators with observablevalues..
|
||||
_sectionIterator = new BiIterator<Section>(_dataModel.sections);
|
||||
_feedIterator = new BiIterator<Feed>(_sectionIterator.current.feeds);
|
||||
_articleIterator = new BiIterator<Article>(_feedIterator.current.articles);
|
||||
_sectionIterator = BiIterator<Section>(_dataModel.sections);
|
||||
_feedIterator = BiIterator<Feed>(_sectionIterator.current.feeds);
|
||||
_articleIterator = BiIterator<Article>(_feedIterator.current.articles);
|
||||
|
||||
currentArticle.addChangeListener((e) {
|
||||
_articleIterator.jumpToValue(currentArticle.value);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an event to fire on any state change
|
||||
*
|
||||
* TODO(jmesserly): fix this so we don't have to enumerate all of our fields
|
||||
* again. One idea here is UIState becomes Observable, Observables have
|
||||
* parents and notifications bubble up the parent chain.
|
||||
*/
|
||||
/// Registers an event to fire on any state change
|
||||
///
|
||||
/// TODO(jmesserly): fix this so we don't have to enumerate all of our fields
|
||||
/// again. One idea here is UIState becomes Observable, Observables have
|
||||
/// parents and notifications bubble up the parent chain.
|
||||
void addChangeListener(ChangeListener listener) {
|
||||
_sectionIterator.currentIndex.addChangeListener(listener);
|
||||
_feedIterator.currentIndex.addChangeListener(listener);
|
||||
|
@ -86,6 +69,7 @@ class SwarmState extends UIState {
|
|||
currentArticle.addChangeListener(listener);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, String> toHistory() {
|
||||
final data = {};
|
||||
data['section'] = currentSection.id;
|
||||
|
@ -96,97 +80,83 @@ class SwarmState extends UIState {
|
|||
return data;
|
||||
}
|
||||
|
||||
@override
|
||||
void loadFromHistory(Map values) {
|
||||
// TODO(jimhug): There's a better way of doing this...
|
||||
if (values['section'] != null) {
|
||||
_sectionIterator
|
||||
.jumpToValue(_dataModel.findSectionById(values['section']));
|
||||
} else {
|
||||
_sectionIterator = new BiIterator<Section>(_dataModel.sections);
|
||||
_sectionIterator = BiIterator<Section>(_dataModel.sections);
|
||||
}
|
||||
if (values['feed'] != null && currentSection != null) {
|
||||
_feedIterator.jumpToValue(currentSection.findFeed(values['feed']));
|
||||
} else {
|
||||
_feedIterator = new BiIterator<Feed>(_sectionIterator.current.feeds);
|
||||
_feedIterator = BiIterator<Feed>(_sectionIterator.current.feeds);
|
||||
}
|
||||
if (values['article'] != null && currentFeed != null) {
|
||||
currentArticle.value = currentFeed.findArticle(values['article']);
|
||||
_articleIterator.jumpToValue(currentArticle.value);
|
||||
} else {
|
||||
_articleIterator =
|
||||
new BiIterator<Article>(_feedIterator.current.articles);
|
||||
_articleIterator = BiIterator<Article>(_feedIterator.current.articles);
|
||||
currentArticle.value = null;
|
||||
}
|
||||
|
||||
storyMaximized.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the currentArticle pointer to the next item in the Feed.
|
||||
*/
|
||||
/// Move the currentArticle pointer to the next item in the Feed.
|
||||
void goToNextArticle() {
|
||||
currentArticle.value = _articleIterator.next();
|
||||
selectedArticle.value = _articleIterator.current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the currentArticle pointer to the previous item in the Feed.
|
||||
*/
|
||||
/// Move the currentArticle pointer to the previous item in the Feed.
|
||||
void goToPreviousArticle() {
|
||||
currentArticle.value = _articleIterator.previous();
|
||||
selectedArticle.value = _articleIterator.current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the selectedArticle pointer to the next item in the Feed.
|
||||
*/
|
||||
/// Move the selectedArticle pointer to the next item in the Feed.
|
||||
void goToNextSelectedArticle() {
|
||||
selectedArticle.value = _articleIterator.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the selectedArticle pointer to the previous item in the Feed.
|
||||
*/
|
||||
/// Move the selectedArticle pointer to the previous item in the Feed.
|
||||
void goToPreviousSelectedArticle() {
|
||||
selectedArticle.value = _articleIterator.previous();
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the pointers for selectedArticle to point to the next
|
||||
* Feed.
|
||||
*/
|
||||
/// Move the pointers for selectedArticle to point to the next
|
||||
/// Feed.
|
||||
void goToNextFeed() {
|
||||
var newFeed = _feedIterator.next();
|
||||
int oldIndex = _articleIterator.currentIndex.value;
|
||||
|
||||
_articleIterator = new BiIterator<Article>(
|
||||
_articleIterator = BiIterator<Article>(
|
||||
newFeed.articles, _articleIterator.currentIndex.listeners);
|
||||
|
||||
_articleIterator.currentIndex.value = oldIndex;
|
||||
selectedArticle.value = _articleIterator.current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the pointers for selectedArticle to point to the previous
|
||||
* DataSource.
|
||||
*/
|
||||
/// Move the pointers for selectedArticle to point to the previous
|
||||
/// DataSource.
|
||||
void goToPreviousFeed() {
|
||||
var newFeed = _feedIterator.previous();
|
||||
int oldIndex = _articleIterator.currentIndex.value;
|
||||
|
||||
_articleIterator = new BiIterator<Article>(
|
||||
_articleIterator = BiIterator<Article>(
|
||||
newFeed.articles, _articleIterator.currentIndex.listeners);
|
||||
_articleIterator.currentIndex.value = oldIndex;
|
||||
selectedArticle.value = _articleIterator.current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move to the next section (page) of feeds in the UI.
|
||||
* @param index the previous index (how far down in a given feed)
|
||||
* from the Source we are moving from.
|
||||
* This method takes sliderMenu in the event that it needs to move
|
||||
* to a previous section, it can notify the UI to update.
|
||||
*/
|
||||
/// Move to the next section (page) of feeds in the UI.
|
||||
/// @param index the previous index (how far down in a given feed)
|
||||
/// from the Source we are moving from.
|
||||
/// This method takes sliderMenu in the event that it needs to move
|
||||
/// to a previous section, it can notify the UI to update.
|
||||
void goToNextSection(SliderMenu sliderMenu) {
|
||||
//TODO(efortuna): move sections?
|
||||
var oldSection = currentSection;
|
||||
|
@ -195,24 +165,22 @@ class SwarmState extends UIState {
|
|||
// This check prevents our selector from wrapping around when we try to
|
||||
// go to the "next section", but we're already at the last section.
|
||||
if (oldSection != _sectionIterator.current) {
|
||||
_feedIterator = new BiIterator<Feed>(
|
||||
_feedIterator = BiIterator<Feed>(
|
||||
_sectionIterator.current.feeds, _feedIterator.currentIndex.listeners);
|
||||
_articleIterator = new BiIterator<Article>(_feedIterator.current.articles,
|
||||
_articleIterator = BiIterator<Article>(_feedIterator.current.articles,
|
||||
_articleIterator.currentIndex.listeners);
|
||||
_articleIterator.currentIndex.value = oldIndex;
|
||||
selectedArticle.value = _articleIterator.current;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move to the previous section (page) of feeds in the UI.
|
||||
* @param index the previous index (how far down in a given feed)
|
||||
* from the Source we are moving from.
|
||||
* @param oldSection the original starting section (before the slider
|
||||
* menu moved)
|
||||
* This method takes sliderMenu in the event that it needs to move
|
||||
* to a previous section, it can notify the UI to update.
|
||||
*/
|
||||
/// Move to the previous section (page) of feeds in the UI.
|
||||
/// @param index the previous index (how far down in a given feed)
|
||||
/// from the Source we are moving from.
|
||||
/// @param oldSection the original starting section (before the slider
|
||||
/// menu moved)
|
||||
/// This method takes sliderMenu in the event that it needs to move
|
||||
/// to a previous section, it can notify the UI to update.
|
||||
void goToPreviousSection(SliderMenu sliderMenu) {
|
||||
//TODO(efortuna): don't pass sliderMenu here. Just update in view!
|
||||
var oldSection = currentSection;
|
||||
|
@ -222,72 +190,58 @@ class SwarmState extends UIState {
|
|||
// This check prevents our selector from wrapping around when we try to
|
||||
// go to the "previous section", but we're already at the first section.
|
||||
if (oldSection != _sectionIterator.current) {
|
||||
_feedIterator = new BiIterator<Feed>(
|
||||
_feedIterator = BiIterator<Feed>(
|
||||
_sectionIterator.current.feeds, _feedIterator.currentIndex.listeners);
|
||||
// Jump to back of feed set if we are moving backwards through sections.
|
||||
_feedIterator.currentIndex.value = _feedIterator.list.length - 1;
|
||||
_articleIterator = new BiIterator<Article>(_feedIterator.current.articles,
|
||||
_articleIterator = BiIterator<Article>(_feedIterator.current.articles,
|
||||
_articleIterator.currentIndex.listeners);
|
||||
_articleIterator.currentIndex.value = oldIndex;
|
||||
selectedArticle.value = _articleIterator.current;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the selected story as the current story (for viewing in the larger
|
||||
* Story View.)
|
||||
*/
|
||||
/// Set the selected story as the current story (for viewing in the larger
|
||||
/// Story View.)
|
||||
void selectStoryAsCurrent() {
|
||||
currentArticle.value = _articleIterator.current;
|
||||
selectedArticle.value = _articleIterator.current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove our currentArticle selection, to move back to the Main Grid view.
|
||||
*/
|
||||
/// Remove our currentArticle selection, to move back to the Main Grid view.
|
||||
void clearCurrentArticle() {
|
||||
currentArticle.value = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the selectedArticle as the first item in that section (UI page).
|
||||
*/
|
||||
/// Set the selectedArticle as the first item in that section (UI page).
|
||||
void goToFirstArticleInSection() {
|
||||
selectedArticle.value = _articleIterator.current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the UI is currently in the Story View state.
|
||||
*/
|
||||
/// Returns true if the UI is currently in the Story View state.
|
||||
bool get inMainView => currentArticle.value == null;
|
||||
|
||||
/**
|
||||
* Returns true if we currently have an Article selected (for keyboard
|
||||
* shortcuts browsing).
|
||||
*/
|
||||
/// Returns true if we currently have an Article selected (for keyboard
|
||||
/// shortcuts browsing).
|
||||
bool get hasArticleSelected => selectedArticle.value != null;
|
||||
|
||||
/**
|
||||
* Mark the current article as read
|
||||
*/
|
||||
/// Mark the current article as read
|
||||
bool markCurrentAsRead() {
|
||||
currentArticle.value.unread.value = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The user has moved to a new section (page). This can occur either
|
||||
* if the user clicked on a section page, or used keyboard shortcuts.
|
||||
* The default behavior is to move to the first article in the first
|
||||
* column. The location of the selected item depends on the previous
|
||||
* selected item location if the user used keyboard shortcuts. These
|
||||
* are manipulated in goToPrevious/NextSection().
|
||||
*/
|
||||
/// The user has moved to a new section (page). This can occur either
|
||||
/// if the user clicked on a section page, or used keyboard shortcuts.
|
||||
/// The default behavior is to move to the first article in the first
|
||||
/// column. The location of the selected item depends on the previous
|
||||
/// selected item location if the user used keyboard shortcuts. These
|
||||
/// are manipulated in goToPrevious/NextSection().
|
||||
void moveToNewSection(String sectionTitle) {
|
||||
_sectionIterator.currentIndex.value =
|
||||
_dataModel.findSectionIndex(sectionTitle);
|
||||
_feedIterator = new BiIterator<Feed>(
|
||||
_feedIterator = BiIterator<Feed>(
|
||||
_sectionIterator.current.feeds, _feedIterator.currentIndex.listeners);
|
||||
_articleIterator = new BiIterator<Article>(_feedIterator.current.articles,
|
||||
_articleIterator = BiIterator<Article>(_feedIterator.current.articles,
|
||||
_articleIterator.currentIndex.listeners);
|
||||
}
|
||||
|
||||
|
|
|
@ -10,42 +10,35 @@ part of swarmlib;
|
|||
// and then doing a large pass to remove functionality that doesn't make sense
|
||||
// given the UI layout.
|
||||
|
||||
/**
|
||||
* Front page of Swarm.
|
||||
*/
|
||||
/// Front page of Swarm.
|
||||
// TODO(jacobr): this code now needs a large refactoring.
|
||||
// Suggested refactorings:
|
||||
// Move animation specific code into helper classes.
|
||||
class FrontView extends CompositeView {
|
||||
final Swarm swarm;
|
||||
|
||||
/** View containing all UI anchored to the top of the page. */
|
||||
/// View containing all UI anchored to the top of the page. */
|
||||
CompositeView topView;
|
||||
/** View containing all UI anchored to the left side of the page. */
|
||||
|
||||
/// View containing all UI anchored to the left side of the page. */
|
||||
CompositeView bottomView;
|
||||
HeaderView headerView;
|
||||
SliderMenu sliderMenu;
|
||||
|
||||
/**
|
||||
* When the user is viewing a story, the data source for that story is
|
||||
* detached from the section and shown at the bottom of the screen. This keeps
|
||||
* track of that so we can restore it later.
|
||||
*/
|
||||
/// When the user is viewing a story, the data source for that story is
|
||||
/// detached from the section and shown at the bottom of the screen. This keeps
|
||||
/// track of that so we can restore it later.
|
||||
DataSourceView detachedView;
|
||||
|
||||
/**
|
||||
* Map from section title to the View that shows this section. This
|
||||
* is populated lazily.
|
||||
*/
|
||||
/// Map from section title to the View that shows this section. This
|
||||
/// is populated lazily.
|
||||
StoryContentView storyView;
|
||||
bool nextPrevShown;
|
||||
|
||||
ConveyorView sections;
|
||||
|
||||
/**
|
||||
* The set of keys that produce a given behavior (going down one story,
|
||||
* navigating to the column to the right, etc).
|
||||
*/
|
||||
/// The set of keys that produce a given behavior (going down one story,
|
||||
/// navigating to the column to the right, etc).
|
||||
//TODO(jmesserly): we need a key code enumeration
|
||||
final Set downKeyPresses;
|
||||
final Set upKeyPresses;
|
||||
|
@ -57,22 +50,22 @@ class FrontView extends CompositeView {
|
|||
final Set previousPageKeyPresses;
|
||||
|
||||
FrontView(this.swarm)
|
||||
: downKeyPresses = new Set.from([74 /*j*/, 40 /*down*/]),
|
||||
upKeyPresses = new Set.from([75 /*k*/, 38 /*up*/]),
|
||||
rightKeyPresses = new Set.from([39 /*right*/, 68 /*d*/, 76 /*l*/]),
|
||||
leftKeyPresses = new Set.from([37 /*left*/, 65 /*a*/, 72 /*h*/]),
|
||||
openKeyPresses = new Set.from([13 /*enter*/, 79 /*o*/]),
|
||||
backKeyPresses = new Set.from([8 /*delete*/, 27 /*escape*/]),
|
||||
nextPageKeyPresses = new Set.from([78 /*n*/]),
|
||||
previousPageKeyPresses = new Set.from([80 /*p*/]),
|
||||
: downKeyPresses = {74 /*j*/, 40 /*down*/},
|
||||
upKeyPresses = {75 /*k*/, 38 /*up*/},
|
||||
rightKeyPresses = {39 /*right*/, 68 /*d*/, 76 /*l*/},
|
||||
leftKeyPresses = {37 /*left*/, 65 /*a*/, 72 /*h*/},
|
||||
openKeyPresses = {13 /*enter*/, 79 /*o*/},
|
||||
backKeyPresses = {8 /*delete*/, 27 /*escape*/},
|
||||
nextPageKeyPresses = {78 /*n*/},
|
||||
previousPageKeyPresses = {80 /*p*/},
|
||||
nextPrevShown = false,
|
||||
super('front-view fullpage') {
|
||||
topView = new CompositeView('top-view', false, false, false);
|
||||
topView = CompositeView('top-view', false, false, false);
|
||||
|
||||
headerView = new HeaderView(swarm);
|
||||
headerView = HeaderView(swarm);
|
||||
topView.addChild(headerView);
|
||||
|
||||
sliderMenu = new SliderMenu(swarm.sections.sectionTitles, (sectionTitle) {
|
||||
sliderMenu = SliderMenu(swarm.sections.sectionTitles, (sectionTitle) {
|
||||
swarm.state.moveToNewSection(sectionTitle);
|
||||
_onSectionSelected(sectionTitle);
|
||||
// Start with no articles selected.
|
||||
|
@ -81,10 +74,10 @@ class FrontView extends CompositeView {
|
|||
topView.addChild(sliderMenu);
|
||||
addChild(topView);
|
||||
|
||||
bottomView = new CompositeView('bottom-view', false, false, false);
|
||||
bottomView = CompositeView('bottom-view', false, false, false);
|
||||
addChild(bottomView);
|
||||
|
||||
sections = new ConveyorView();
|
||||
sections = ConveyorView();
|
||||
sections.viewSelected = _onSectionTransitionEnded;
|
||||
}
|
||||
|
||||
|
@ -98,6 +91,7 @@ class FrontView extends CompositeView {
|
|||
return view;
|
||||
}
|
||||
|
||||
@override
|
||||
void afterRender(Element node) {
|
||||
_createSectionViews();
|
||||
attachWatch(swarm.state.currentArticle, (e) {
|
||||
|
@ -116,9 +110,7 @@ class FrontView extends CompositeView {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Animates back from the story view to the main grid view.
|
||||
*/
|
||||
/// Animates back from the story view to the main grid view.
|
||||
void _animateToMainView() {
|
||||
sliderMenu.removeClass('hidden');
|
||||
storyView.addClass('hidden-story');
|
||||
|
@ -156,7 +148,7 @@ class FrontView extends CompositeView {
|
|||
removeChild(storyView);
|
||||
|
||||
// Create the new story view and place in the frame.
|
||||
storyView = addChild(new StoryContentView(swarm, item));
|
||||
storyView = addChild(StoryContentView(swarm, item));
|
||||
} else {
|
||||
// We are animating from the main view to the story view.
|
||||
// TODO(jmesserly): make this code better
|
||||
|
@ -173,16 +165,16 @@ class FrontView extends CompositeView {
|
|||
currentSection.storyMode = true;
|
||||
|
||||
// Create the new story view.
|
||||
storyView = new StoryContentView(swarm, item);
|
||||
new Timer(const Duration(milliseconds: 0), () {
|
||||
storyView = StoryContentView(swarm, item);
|
||||
Timer(const Duration(milliseconds: 0), () {
|
||||
_animateDataSourceToMinimized();
|
||||
|
||||
sliderMenu.addClass('hidden');
|
||||
// Make the fancy sliding into the window animation.
|
||||
new Timer(const Duration(milliseconds: 0), () {
|
||||
Timer(const Duration(milliseconds: 0), () {
|
||||
storyView.addClass('hidden-story');
|
||||
addChild(storyView);
|
||||
new Timer(const Duration(milliseconds: 0), () {
|
||||
Timer(const Duration(milliseconds: 0), () {
|
||||
storyView.removeClass('hidden-story');
|
||||
});
|
||||
headerView.endTransitionToStoryView();
|
||||
|
@ -214,9 +206,7 @@ class FrontView extends CompositeView {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the animation to switch to a section has completed.
|
||||
*/
|
||||
/// Called when the animation to switch to a section has completed.
|
||||
void _onSectionTransitionEnded(SectionView selectedView) {
|
||||
// Show the section and hide the others.
|
||||
for (SectionView view in sections.childViews) {
|
||||
|
@ -230,10 +220,8 @@ class FrontView extends CompositeView {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user chooses a section on the SliderMenu. Hides
|
||||
* all views except the one they want to see.
|
||||
*/
|
||||
/// Called when the user chooses a section on the SliderMenu. Hides
|
||||
/// all views except the one they want to see.
|
||||
void _onSectionSelected(String sectionTitle) {
|
||||
final section = swarm.sections.findSection(sectionTitle);
|
||||
// Find the view for this section.
|
||||
|
@ -246,15 +234,13 @@ class FrontView extends CompositeView {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create SectionViews for each Section in the app and add them to the
|
||||
* conveyor. Note that the SectionViews won't actually populate or load data
|
||||
* sources until they are shown in response to [:_onSectionSelected():].
|
||||
*/
|
||||
/// Create SectionViews for each Section in the app and add them to the
|
||||
/// conveyor. Note that the SectionViews won't actually populate or load data
|
||||
/// sources until they are shown in response to [:_onSectionSelected():].
|
||||
void _createSectionViews() {
|
||||
for (final section in swarm.sections) {
|
||||
final viewFactory = new DataSourceViewFactory(swarm);
|
||||
final sectionView = new SectionView(swarm, section, viewFactory);
|
||||
final viewFactory = DataSourceViewFactory(swarm);
|
||||
final sectionView = SectionView(swarm, section, viewFactory);
|
||||
|
||||
// TODO(rnystrom): Hack temp. Access node to make sure SectionView has
|
||||
// rendered and created scroller. This can go away when event registration
|
||||
|
@ -266,10 +252,8 @@ class FrontView extends CompositeView {
|
|||
addChild(sections);
|
||||
}
|
||||
|
||||
/**
|
||||
* Controls the logic of how to respond to keypresses and then update the
|
||||
* UI accordingly.
|
||||
*/
|
||||
/// Controls the logic of how to respond to keypresses and then update the
|
||||
/// UI accordingly.
|
||||
void processKeyEvent(KeyboardEvent e) {
|
||||
int code = e.keyCode;
|
||||
if (swarm.state.inMainView) {
|
||||
|
@ -313,7 +297,7 @@ class FrontView extends CompositeView {
|
|||
}
|
||||
}
|
||||
|
||||
/** Transitions the app back to the main screen. */
|
||||
/// Transitions the app back to the main screen. */
|
||||
void _backToMain(SwarmState state) {
|
||||
if (state.currentArticle.value != null) {
|
||||
state.clearCurrentArticle();
|
||||
|
@ -322,14 +306,16 @@ void _backToMain(SwarmState state) {
|
|||
}
|
||||
}
|
||||
|
||||
/** A back button that sends the user back to the front page. */
|
||||
/// A back button that sends the user back to the front page. */
|
||||
class SwarmBackButton extends View {
|
||||
Swarm swarm;
|
||||
|
||||
SwarmBackButton(this.swarm);
|
||||
|
||||
Element render() => new Element.html('<div class="back-arrow button"></div>');
|
||||
@override
|
||||
Element render() => Element.html('<div class="back-arrow button"></div>');
|
||||
|
||||
@override
|
||||
void afterRender(Element node) {
|
||||
addOnClick((e) {
|
||||
_backToMain(swarm.state);
|
||||
|
@ -337,7 +323,7 @@ class SwarmBackButton extends View {
|
|||
}
|
||||
}
|
||||
|
||||
/** Top view constaining the title and standard buttons. */
|
||||
/// Top view constaining the title and standard buttons. */
|
||||
class HeaderView extends CompositeView {
|
||||
// TODO(jacobr): make this value be coupled with the CSS file.
|
||||
static const HEIGHT = 80;
|
||||
|
@ -357,7 +343,7 @@ class HeaderView extends CompositeView {
|
|||
View _newWindowButton;
|
||||
|
||||
HeaderView(this.swarm) : super('header-view') {
|
||||
_backButton = addChild(new SwarmBackButton(swarm));
|
||||
_backButton = addChild(SwarmBackButton(swarm));
|
||||
_title = addChild(View.div('app-title', 'Swarm'));
|
||||
_configButton = addChild(View.div('config button'));
|
||||
_refreshButton = addChild(View.div('refresh button'));
|
||||
|
@ -365,11 +351,12 @@ class HeaderView extends CompositeView {
|
|||
|
||||
// TODO(rnystrom): No more web/text mode (it's just text) so get rid of
|
||||
// these.
|
||||
_webBackButton = addChild(new WebBackButton());
|
||||
_webForwardButton = addChild(new WebForwardButton());
|
||||
_webBackButton = addChild(WebBackButton());
|
||||
_webForwardButton = addChild(WebForwardButton());
|
||||
_newWindowButton = addChild(View.div('new-window-button button'));
|
||||
}
|
||||
|
||||
@override
|
||||
void afterRender(Element node) {
|
||||
// Respond to changes to whether the story is being shown as text or web.
|
||||
attachWatch(swarm.state.storyTextMode, (e) {
|
||||
|
@ -383,18 +370,18 @@ class HeaderView extends CompositeView {
|
|||
// Wire up the events.
|
||||
_configButton.addOnClick((e) {
|
||||
// Bring up the config dialog.
|
||||
if (this._configDialog == null) {
|
||||
if (_configDialog == null) {
|
||||
// TODO(terry): Cleanup, HeaderView shouldn't be tangled with main view.
|
||||
this._configDialog = new ConfigHintDialog(swarm.frontView, () {
|
||||
swarm.frontView.removeChild(this._configDialog);
|
||||
this._configDialog = null;
|
||||
_configDialog = ConfigHintDialog(swarm.frontView, () {
|
||||
swarm.frontView.removeChild(_configDialog);
|
||||
_configDialog = null;
|
||||
|
||||
// TODO: Need to push these to the server on a per-user basis.
|
||||
// Update the storage now.
|
||||
swarm.sections.refresh();
|
||||
});
|
||||
|
||||
swarm.frontView.addChild(this._configDialog);
|
||||
swarm.frontView.addChild(_configDialog);
|
||||
}
|
||||
// TODO(jimhug): Graceful redirection to reader.
|
||||
});
|
||||
|
@ -407,16 +394,16 @@ class HeaderView extends CompositeView {
|
|||
// On click of the info button, show Dart info page in new window/tab.
|
||||
_infoButton.addOnClick((e) {
|
||||
// Bring up the config dialog.
|
||||
if (this._infoDialog == null) {
|
||||
if (_infoDialog == null) {
|
||||
// TODO(terry): Cleanup, HeaderView shouldn't be tangled with main view.
|
||||
this._infoDialog = new HelpDialog(swarm.frontView, () {
|
||||
swarm.frontView.removeChild(this._infoDialog);
|
||||
this._infoDialog = null;
|
||||
_infoDialog = HelpDialog(swarm.frontView, () {
|
||||
swarm.frontView.removeChild(_infoDialog);
|
||||
_infoDialog = null;
|
||||
|
||||
swarm.sections.refresh();
|
||||
});
|
||||
|
||||
swarm.frontView.addChild(this._infoDialog);
|
||||
swarm.frontView.addChild(_infoDialog);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -429,10 +416,8 @@ class HeaderView extends CompositeView {
|
|||
startTransitionToMainView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes whether or not the buttons specific to the display of a story in
|
||||
* the web perspective are visible.
|
||||
*/
|
||||
/// Refreshes whether or not the buttons specific to the display of a story in
|
||||
/// the web perspective are visible.
|
||||
void refreshWebStoryButtons() {
|
||||
bool webButtonsHidden = true;
|
||||
|
||||
|
@ -467,70 +452,73 @@ class HeaderView extends CompositeView {
|
|||
}
|
||||
}
|
||||
|
||||
/** A back button for the web view of a story that is equivalent to clicking
|
||||
* "back" in the browser. */
|
||||
/// A back button for the web view of a story that is equivalent to clicking
|
||||
/// "back" in the browser. */
|
||||
// TODO(rnystrom): We have nearly identical versions of this littered through
|
||||
// the sample apps. Should consolidate into one.
|
||||
class WebBackButton extends View {
|
||||
WebBackButton();
|
||||
|
||||
@override
|
||||
Element render() {
|
||||
return new Element.html('<div class="web-back-button button"></div>');
|
||||
return Element.html('<div class="web-back-button button"></div>');
|
||||
}
|
||||
|
||||
@override
|
||||
void afterRender(Element node) {
|
||||
addOnClick((e) {
|
||||
back();
|
||||
});
|
||||
}
|
||||
|
||||
/** Equivalent to [window.history.back] */
|
||||
/// Equivalent to [window.history.back] */
|
||||
static void back() {
|
||||
window.history.back();
|
||||
}
|
||||
}
|
||||
|
||||
/** A back button for the web view of a story that is equivalent to clicking
|
||||
* "forward" in the browser. */
|
||||
/// A back button for the web view of a story that is equivalent to clicking
|
||||
/// "forward" in the browser. */
|
||||
// TODO(rnystrom): We have nearly identical versions of this littered through
|
||||
// the sample apps. Should consolidate into one.
|
||||
class WebForwardButton extends View {
|
||||
WebForwardButton();
|
||||
|
||||
@override
|
||||
Element render() {
|
||||
return new Element.html('<div class="web-forward-button button"></div>');
|
||||
return Element.html('<div class="web-forward-button button"></div>');
|
||||
}
|
||||
|
||||
@override
|
||||
void afterRender(Element node) {
|
||||
addOnClick((e) {
|
||||
forward();
|
||||
});
|
||||
}
|
||||
|
||||
/** Equivalent to [window.history.forward] */
|
||||
/// Equivalent to [window.history.forward] */
|
||||
static void forward() {
|
||||
window.history.forward();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A factory that creates a view for data sources.
|
||||
*/
|
||||
/// A factory that creates a view for data sources.
|
||||
class DataSourceViewFactory implements ViewFactory<Feed> {
|
||||
Swarm swarm;
|
||||
|
||||
DataSourceViewFactory(this.swarm) {}
|
||||
DataSourceViewFactory(this.swarm);
|
||||
|
||||
View newView(Feed data) => new DataSourceView(data, swarm);
|
||||
@override
|
||||
View newView(Feed data) => DataSourceView(data, swarm);
|
||||
|
||||
@override
|
||||
int get width => ArticleViewLayout.getSingleton().width;
|
||||
@override
|
||||
int get height => null; // Width for this view isn't known.
|
||||
}
|
||||
|
||||
/**
|
||||
* A view for the items from a single data source.
|
||||
* Shows a title and a list of items.
|
||||
*/
|
||||
/// A view for the items from a single data source.
|
||||
/// Shows a title and a list of items.
|
||||
class DataSourceView extends CompositeView {
|
||||
// TODO(jacobr): make this value be coupled with the CSS file.
|
||||
static const TAB_ONLY_HEIGHT = 34;
|
||||
|
@ -541,12 +529,12 @@ class DataSourceView extends CompositeView {
|
|||
DataSourceView(this.source, Swarm swarm) : super('query') {
|
||||
// TODO(jacobr): make the title a view or decide it is sane for a subclass
|
||||
// of component view to manually add some DOM cruft.
|
||||
node.nodes.add(new Element.html('<h2>${source.title}</h2>'));
|
||||
node.nodes.add(Element.html('<h2>${source.title}</h2>'));
|
||||
|
||||
// TODO(jacobr): use named arguments when available.
|
||||
itemsView = addChild(new VariableSizeListView<Article>(
|
||||
itemsView = addChild(VariableSizeListView<Article>(
|
||||
source.articles,
|
||||
new ArticleViewFactory(swarm),
|
||||
ArticleViewFactory(swarm),
|
||||
true,
|
||||
/* scrollable */
|
||||
true,
|
||||
|
@ -559,7 +547,7 @@ class DataSourceView extends CompositeView {
|
|||
!Device.supportsTouch /* showScrollbar */));
|
||||
itemsView.addClass('story-section');
|
||||
|
||||
node.nodes.add(new Element.html('<div class="query-name-shadow"></div>'));
|
||||
node.nodes.add(Element.html('<div class="query-name-shadow"></div>'));
|
||||
|
||||
// Clicking the view (i.e. its title area) unmaximizes to show the entire
|
||||
// view.
|
||||
|
@ -569,15 +557,17 @@ class DataSourceView extends CompositeView {
|
|||
}
|
||||
}
|
||||
|
||||
/** A button that toggles between states. */
|
||||
/// A button that toggles between states. */
|
||||
class ToggleButton extends View {
|
||||
EventListeners onChanged;
|
||||
List<String> states;
|
||||
|
||||
ToggleButton(this.states) : onChanged = new EventListeners();
|
||||
ToggleButton(this.states) : onChanged = EventListeners();
|
||||
|
||||
Element render() => new Element.tag('button');
|
||||
@override
|
||||
Element render() => Element.tag('button');
|
||||
|
||||
@override
|
||||
void afterRender(Element node) {
|
||||
state = states[0];
|
||||
node.onClick.listen((event) {
|
||||
|
@ -587,12 +577,12 @@ class ToggleButton extends View {
|
|||
|
||||
String get state {
|
||||
final currentState = node.innerHtml;
|
||||
assert(states.indexOf(currentState, 0) >= 0);
|
||||
assert(states.contains(currentState));
|
||||
return currentState;
|
||||
}
|
||||
|
||||
void set state(String state) {
|
||||
assert(states.indexOf(state, 0) >= 0);
|
||||
set state(String state) {
|
||||
assert(states.contains(state));
|
||||
node.innerHtml = state;
|
||||
onChanged.fire(null);
|
||||
}
|
||||
|
@ -605,18 +595,19 @@ class ToggleButton extends View {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A factory that creates a view for generic items.
|
||||
*/
|
||||
/// A factory that creates a view for generic items.
|
||||
class ArticleViewFactory implements VariableSizeViewFactory<Article> {
|
||||
Swarm swarm;
|
||||
|
||||
ArticleViewLayout layout;
|
||||
ArticleViewFactory(this.swarm) : layout = ArticleViewLayout.getSingleton();
|
||||
|
||||
View newView(Article item) => new ArticleView(item, swarm, layout);
|
||||
@override
|
||||
View newView(Article item) => ArticleView(item, swarm, layout);
|
||||
|
||||
@override
|
||||
int getWidth(Article item) => layout.width;
|
||||
@override
|
||||
int getHeight(Article item) => layout.computeHeight(item);
|
||||
}
|
||||
|
||||
|
@ -650,16 +641,14 @@ class ArticleViewLayout {
|
|||
int width;
|
||||
static ArticleViewLayout _singleton;
|
||||
ArticleViewLayout()
|
||||
: measureBodyText = new MeasureText(BODY_FONT),
|
||||
measureTitleText = new MeasureText(TITLE_FONT) {
|
||||
: measureBodyText = MeasureText(BODY_FONT),
|
||||
measureTitleText = MeasureText(TITLE_FONT) {
|
||||
num screenWidth = window.screen.width;
|
||||
width = DESKTOP_WIDTH;
|
||||
}
|
||||
|
||||
static ArticleViewLayout getSingleton() {
|
||||
if (_singleton == null) {
|
||||
_singleton = new ArticleViewLayout();
|
||||
}
|
||||
_singleton ??= ArticleViewLayout();
|
||||
return _singleton;
|
||||
}
|
||||
|
||||
|
@ -673,10 +662,8 @@ class ArticleViewLayout {
|
|||
return computeLayout(item, null, null).height;
|
||||
}
|
||||
|
||||
/**
|
||||
* titleContainer and snippetContainer may be null in which case the size is
|
||||
* computed but no actual layout is performed.
|
||||
*/
|
||||
/// titleContainer and snippetContainer may be null in which case the size is
|
||||
/// computed but no actual layout is performed.
|
||||
ArticleViewMetrics computeLayout(
|
||||
Article item, StringBuffer titleBuffer, StringBuffer snippetBuffer) {
|
||||
int titleWidth = width - BODY_MARGIN_LEFT;
|
||||
|
@ -696,13 +683,11 @@ class ArticleViewLayout {
|
|||
height = 92;
|
||||
}
|
||||
|
||||
return new ArticleViewMetrics(height, titleLines, bodyLines);
|
||||
return ArticleViewMetrics(height, titleLines, bodyLines);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A view for a generic item.
|
||||
*/
|
||||
/// A view for a generic item.
|
||||
class ArticleView extends View {
|
||||
// Set to false to make inspecting the HTML more pleasant...
|
||||
static const SAVE_IMAGES = false;
|
||||
|
@ -713,10 +698,11 @@ class ArticleView extends View {
|
|||
|
||||
ArticleView(this.item, this.swarm, this.articleLayout);
|
||||
|
||||
@override
|
||||
Element render() {
|
||||
Element node;
|
||||
|
||||
final byline = item.author.length > 0 ? item.author : item.dataSource.title;
|
||||
final byline = item.author.isNotEmpty ? item.author : item.dataSource.title;
|
||||
final date = DateUtils.toRecentTimeString(item.date);
|
||||
|
||||
String storyClass = 'story no-thumb';
|
||||
|
@ -727,13 +713,13 @@ class ArticleView extends View {
|
|||
thumbnail = '<img src="${item.thumbUrl}"></img>';
|
||||
}
|
||||
|
||||
final title = new StringBuffer();
|
||||
final snippet = new StringBuffer();
|
||||
final title = StringBuffer();
|
||||
final snippet = StringBuffer();
|
||||
|
||||
// Note: also populates title and snippet elements.
|
||||
final metrics = articleLayout.computeLayout(item, title, snippet);
|
||||
|
||||
node = new Element.html('''
|
||||
node = Element.html('''
|
||||
<div class="$storyClass">
|
||||
$thumbnail
|
||||
<div class="title">$title</div>
|
||||
|
@ -751,6 +737,7 @@ class ArticleView extends View {
|
|||
return node;
|
||||
}
|
||||
|
||||
@override
|
||||
void afterRender(Element node) {
|
||||
// Select this view's item.
|
||||
addOnClick((e) {
|
||||
|
@ -789,10 +776,8 @@ class ArticleView extends View {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify the view to jump to a different area if we are selecting an
|
||||
* article that is currently outside of the visible area.
|
||||
*/
|
||||
/// Notify the view to jump to a different area if we are selecting an
|
||||
/// article that is currently outside of the visible area.
|
||||
void _updateViewForSelectedArticle() {
|
||||
Article selArticle = swarm.state.selectedArticle.value;
|
||||
if (swarm.state.hasArticleSelected) {
|
||||
|
@ -818,7 +803,7 @@ class ArticleView extends View {
|
|||
String getDataUriForImage(final img) {
|
||||
// TODO(hiltonc,jimhug) eval perf of this vs. reusing one canvas element
|
||||
final CanvasElement canvas =
|
||||
new CanvasElement(height: img.height, width: img.width);
|
||||
CanvasElement(height: img.height, width: img.width);
|
||||
|
||||
final CanvasRenderingContext2D ctx = canvas.getContext("2d");
|
||||
ctx.drawImageScaled(img, 0, 0, img.width, img.height);
|
||||
|
@ -826,10 +811,8 @@ class ArticleView extends View {
|
|||
return canvas.toDataUrl("image/png");
|
||||
}
|
||||
|
||||
/**
|
||||
* Update this view's selected appearance based on the currently selected
|
||||
* Article.
|
||||
*/
|
||||
/// Update this view's selected appearance based on the currently selected
|
||||
/// Article.
|
||||
void _refreshSelected(curItem) {
|
||||
if (curItem.value == item) {
|
||||
addClass('sel');
|
||||
|
@ -843,10 +826,8 @@ class ArticleView extends View {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An internal view of a story as text. In other words, the article is shown
|
||||
* in-place as opposed to as an embedded web-page.
|
||||
*/
|
||||
/// An internal view of a story as text. In other words, the article is shown
|
||||
/// in-place as opposed to as an embedded web-page.
|
||||
class StoryContentView extends View {
|
||||
final Swarm swarm;
|
||||
final Article item;
|
||||
|
@ -855,16 +836,18 @@ class StoryContentView extends View {
|
|||
|
||||
StoryContentView(this.swarm, this.item);
|
||||
|
||||
@override
|
||||
get childViews => [_pagedStory];
|
||||
|
||||
@override
|
||||
Element render() {
|
||||
final storyContent =
|
||||
new Element.html('<div class="story-content">${item.htmlBody}</div>');
|
||||
Element.html('<div class="story-content">${item.htmlBody}</div>');
|
||||
for (Element element in storyContent.querySelectorAll(
|
||||
"iframe, script, style, object, embed, frameset, frame")) {
|
||||
element.remove();
|
||||
}
|
||||
_pagedStory = new PagedContentView(new View.fromNode(storyContent));
|
||||
_pagedStory = PagedContentView(View.fromNode(storyContent));
|
||||
|
||||
// Modify all links to open in new windows....
|
||||
// TODO(jacobr): would it be better to add an event listener on click that
|
||||
|
@ -874,7 +857,7 @@ class StoryContentView extends View {
|
|||
}
|
||||
|
||||
final date = DateUtils.toRecentTimeString(item.date);
|
||||
final container = new Element.html('''
|
||||
final container = Element.html('''
|
||||
<div class="story-view">
|
||||
<div class="story-text-view">
|
||||
<div class="story-header">
|
||||
|
@ -906,22 +889,20 @@ class SectionView extends CompositeView {
|
|||
final PageState pageState;
|
||||
|
||||
SectionView(this.swarm, this.section, this._viewFactory)
|
||||
: loadingText = new View.html('<div class="loading-section"></div>'),
|
||||
pageState = new PageState(),
|
||||
: loadingText = View.html('<div class="loading-section"></div>'),
|
||||
pageState = PageState(),
|
||||
super('section-view') {
|
||||
addChild(loadingText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the loading text, reloads the data sources, and shows them.
|
||||
*/
|
||||
/// Hides the loading text, reloads the data sources, and shows them.
|
||||
void showSources() {
|
||||
loadingText.node.style.display = 'none';
|
||||
|
||||
// Lazy initialize the data source view.
|
||||
if (dataSourceView == null) {
|
||||
// TODO(jacobr): use named arguments when available.
|
||||
dataSourceView = new ListView<Feed>(
|
||||
dataSourceView = ListView<Feed>(
|
||||
section.feeds,
|
||||
_viewFactory,
|
||||
true /* scrollable */,
|
||||
|
@ -936,7 +917,7 @@ class SectionView extends CompositeView {
|
|||
dataSourceView.addClass("data-source-view");
|
||||
addChild(dataSourceView);
|
||||
|
||||
pageNumberView = addChild(new PageNumberView(pageState));
|
||||
pageNumberView = addChild(PageNumberView(pageState));
|
||||
|
||||
node.style.opacity = '1';
|
||||
} else {
|
||||
|
@ -949,9 +930,7 @@ class SectionView extends CompositeView {
|
|||
dataSourceView.scroller.reconfigure(() {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides the data sources and shows the loading text.
|
||||
*/
|
||||
/// Hides the data sources and shows the loading text.
|
||||
void hideSources() {
|
||||
if (dataSourceView != null) {
|
||||
node.style.opacity = '0.6';
|
||||
|
@ -970,10 +949,8 @@ class SectionView extends CompositeView {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the [DataSourceView] in this SectionView that's displaying the given
|
||||
* [Feed].
|
||||
*/
|
||||
/// Find the [DataSourceView] in this SectionView that's displaying the given
|
||||
/// [Feed].
|
||||
DataSourceView findView(Feed dataSource) {
|
||||
return dataSourceView.getSubview(dataSourceView.findIndex(dataSource));
|
||||
}
|
||||
|
|
|
@ -6,14 +6,10 @@
|
|||
|
||||
part of swarmlib;
|
||||
|
||||
/**
|
||||
* The base class for UI state that intends to support browser history.
|
||||
*/
|
||||
/// The base class for UI state that intends to support browser history.
|
||||
abstract class UIState {
|
||||
/**
|
||||
* The event listener we hook to the window's "popstate" event.
|
||||
* This event is triggered by the back button or by the first page load.
|
||||
*/
|
||||
/// The event listener we hook to the window's "popstate" event.
|
||||
/// This event is triggered by the back button or by the first page load.
|
||||
StreamSubscription _historyTracking;
|
||||
|
||||
UIState();
|
||||
|
@ -51,7 +47,7 @@ abstract class UIState {
|
|||
}
|
||||
}
|
||||
|
||||
/** Pushes a state onto the browser history stack */
|
||||
/// Pushes a state onto the browser history stack */
|
||||
void pushToHistory() {
|
||||
if (_historyTracking == null) {
|
||||
throw 'history tracking not started';
|
||||
|
@ -62,17 +58,12 @@ abstract class UIState {
|
|||
// TODO(jmesserly): [state] should be an Object, and we should pass it to
|
||||
// the state parameter instead of as a #hash URL. Right now we're working
|
||||
// around b/4582542.
|
||||
window.history
|
||||
.pushState(null, '${document.title}', '${document.title}#$state');
|
||||
window.history.pushState(null, document.title, '${document.title}#$state');
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize the state to a form suitable for storing in browser history.
|
||||
*/
|
||||
/// Serialize the state to a form suitable for storing in browser history.
|
||||
Map<String, String> toHistory();
|
||||
|
||||
/**
|
||||
* Load the UI state from the given [values].
|
||||
*/
|
||||
/// Load the UI state from the given [values].
|
||||
void loadFromHistory(Map<String, String> values);
|
||||
}
|
||||
|
|
|
@ -9,32 +9,32 @@ part of swarmlib;
|
|||
// This file contains View framework classes.
|
||||
// As it grows, it may need to be split into multiple files.
|
||||
|
||||
/** A factory that creates a view from a data model. */
|
||||
/// A factory that creates a view from a data model. */
|
||||
abstract class ViewFactory<D> {
|
||||
View newView(D item);
|
||||
|
||||
/** The width of the created view or null if the width is not fixed. */
|
||||
/// The width of the created view or null if the width is not fixed. */
|
||||
int get width;
|
||||
|
||||
/** The height of the created view or null if the height is not fixed. */
|
||||
/// The height of the created view or null if the height is not fixed. */
|
||||
int get height;
|
||||
}
|
||||
|
||||
abstract class VariableSizeViewFactory<D> {
|
||||
View newView(D item);
|
||||
|
||||
/** The width of the created view for a specific data model. */
|
||||
/// The width of the created view for a specific data model. */
|
||||
int getWidth(D item);
|
||||
|
||||
/** The height of the created view for a specific data model. */
|
||||
/// The height of the created view for a specific data model. */
|
||||
int getHeight(D item);
|
||||
}
|
||||
|
||||
/** A collection of event listeners. */
|
||||
/// A collection of event listeners. */
|
||||
class EventListeners {
|
||||
var listeners;
|
||||
EventListeners() {
|
||||
listeners = new List();
|
||||
listeners = [];
|
||||
}
|
||||
|
||||
void addListener(listener) {
|
||||
|
@ -48,67 +48,61 @@ class EventListeners {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Private view class used to store placeholder views for detached ListView
|
||||
* elements.
|
||||
*/
|
||||
/// Private view class used to store placeholder views for detached ListView
|
||||
/// elements.
|
||||
class _PlaceholderView extends View {
|
||||
_PlaceholderView();
|
||||
|
||||
Element render() => new Element.tag('div');
|
||||
@override
|
||||
Element render() => Element.tag('div');
|
||||
}
|
||||
|
||||
/**
|
||||
* Class providing all metrics required to layout a data driven list view.
|
||||
*/
|
||||
/// Class providing all metrics required to layout a data driven list view.
|
||||
abstract class ListViewLayout<D> {
|
||||
void onDataChange();
|
||||
|
||||
// TODO(jacobr): placing the newView member function on this class seems like
|
||||
// the wrong design.
|
||||
View newView(int index);
|
||||
/** Get the height of the view. Possibly expensive to compute. */
|
||||
|
||||
/// Get the height of the view. Possibly expensive to compute. */
|
||||
int getHeight(int viewLength);
|
||||
/** Get the width of the view. Possibly expensive to compute. */
|
||||
|
||||
/// Get the width of the view. Possibly expensive to compute. */
|
||||
int getWidth(int viewLength);
|
||||
/** Get the length of the view. Possible expensive to compute. */
|
||||
|
||||
/// Get the length of the view. Possible expensive to compute. */
|
||||
int getLength(int viewLength);
|
||||
/** Estimated height of the view. Guaranteed to be fast to compute. */
|
||||
|
||||
/// Estimated height of the view. Guaranteed to be fast to compute. */
|
||||
int getEstimatedHeight(int viewLength);
|
||||
/** Estimated with of the view. Guaranteed to be fast to compute. */
|
||||
|
||||
/// Estimated with of the view. Guaranteed to be fast to compute. */
|
||||
int getEstimatedWidth(int viewLength);
|
||||
|
||||
/**
|
||||
* Returns the offset in px that the ith item in the view should be placed
|
||||
* at.
|
||||
*/
|
||||
/// Returns the offset in px that the ith item in the view should be placed
|
||||
/// at.
|
||||
int getOffset(int index);
|
||||
|
||||
/**
|
||||
* The page the ith item in the view should be placed in.
|
||||
*/
|
||||
/// The page the ith item in the view should be placed in.
|
||||
int getPage(int index, int viewLength);
|
||||
int getPageStartIndex(int index, int viewLength);
|
||||
|
||||
int getEstimatedLength(int viewLength);
|
||||
/**
|
||||
* Snap a specified index to the nearest visible view given the [viewLength].
|
||||
*/
|
||||
|
||||
/// Snap a specified index to the nearest visible view given the [viewLength].
|
||||
int getSnapIndex(num offset, num viewLength);
|
||||
/**
|
||||
* Returns an interval specifying what views are currently visible given a
|
||||
* particular [:offset:].
|
||||
*/
|
||||
|
||||
/// Returns an interval specifying what views are currently visible given a
|
||||
/// particular [:offset:].
|
||||
Interval computeVisibleInterval(num offset, num viewLength, num bufferLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class used for the simple fixed size item [:ListView:] classes and more
|
||||
* complex list view classes such as [:VariableSizeListView:] using a
|
||||
* [:ListViewLayout:] class to drive the actual layout.
|
||||
*/
|
||||
/// Base class used for the simple fixed size item [:ListView:] classes and more
|
||||
/// complex list view classes such as [:VariableSizeListView:] using a
|
||||
/// [:ListViewLayout:] class to drive the actual layout.
|
||||
class GenericListView<D> extends View {
|
||||
/** Minimum throw distance in pixels to trigger snapping to the next item. */
|
||||
/// Minimum throw distance in pixels to trigger snapping to the next item. */
|
||||
static const SNAP_TO_NEXT_THROW_THRESHOLD = 15;
|
||||
|
||||
static const INDEX_DATA_ATTRIBUTE = 'data-index';
|
||||
|
@ -118,25 +112,24 @@ class GenericListView<D> extends View {
|
|||
final bool _snapToItems;
|
||||
Scroller scroller;
|
||||
Scrollbar _scrollbar;
|
||||
List<D> _data;
|
||||
ObservableValue<D> _selectedItem;
|
||||
Map<int, View> _itemViews;
|
||||
final List<D> _data;
|
||||
final ObservableValue<D> _selectedItem;
|
||||
final Map<int, View> _itemViews;
|
||||
Element _containerElem;
|
||||
bool _vertical;
|
||||
/** Length of the scrollable dimension of the view in px. */
|
||||
final bool _vertical;
|
||||
|
||||
/// Length of the scrollable dimension of the view in px. */
|
||||
int _viewLength = 0;
|
||||
Interval _activeInterval;
|
||||
bool _paginate;
|
||||
bool _removeClippedViews;
|
||||
ListViewLayout<D> _layout;
|
||||
final bool _paginate;
|
||||
final bool _removeClippedViews;
|
||||
final ListViewLayout<D> _layout;
|
||||
D _lastSelectedItem;
|
||||
PageState _pages;
|
||||
final PageState _pages;
|
||||
|
||||
/**
|
||||
* Creates a new GenericListView with the given layout and data. If [:_data:]
|
||||
* is an [:ObservableList<T>:] then it will listen to changes to the list
|
||||
* and update the view appropriately.
|
||||
*/
|
||||
/// Creates a new GenericListView with the given layout and data. If [:_data:]
|
||||
/// is an [:ObservableList<T>:] then it will listen to changes to the list
|
||||
/// and update the view appropriately.
|
||||
GenericListView(
|
||||
this._layout,
|
||||
this._data,
|
||||
|
@ -148,8 +141,8 @@ class GenericListView<D> extends View {
|
|||
this._removeClippedViews,
|
||||
this._showScrollbar,
|
||||
this._pages)
|
||||
: _activeInterval = new Interval(0, 0),
|
||||
_itemViews = new Map<int, View>() {
|
||||
: _activeInterval = Interval(0, 0),
|
||||
_itemViews = <int, View>{} {
|
||||
// TODO(rnystrom): Move this into enterDocument once we have an exitDocument
|
||||
// that we can use to unregister it.
|
||||
if (_scrollable) {
|
||||
|
@ -169,6 +162,7 @@ class GenericListView<D> extends View {
|
|||
_lastSelectedItem = _selectedItem.value;
|
||||
}
|
||||
|
||||
@override
|
||||
Iterable<View> get childViews {
|
||||
return _itemViews.values.toList();
|
||||
}
|
||||
|
@ -194,16 +188,17 @@ class GenericListView<D> extends View {
|
|||
int _nodeToIndex(Element node) {
|
||||
// TODO(jacobr): use data attributes when available.
|
||||
String index = node.attributes[INDEX_DATA_ATTRIBUTE];
|
||||
if (index != null && index.length > 0) {
|
||||
if (index != null && index.isNotEmpty) {
|
||||
return int.parse(index);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Element render() {
|
||||
final node = new Element.tag('div');
|
||||
final node = Element.tag('div');
|
||||
if (_scrollable) {
|
||||
_containerElem = new Element.tag('div');
|
||||
_containerElem = Element.tag('div');
|
||||
_containerElem.tabIndex = -1;
|
||||
node.nodes.add(_containerElem);
|
||||
} else {
|
||||
|
@ -211,16 +206,16 @@ class GenericListView<D> extends View {
|
|||
}
|
||||
|
||||
if (_scrollable) {
|
||||
scroller = new Scroller(
|
||||
scroller = Scroller(
|
||||
_containerElem,
|
||||
_vertical /* verticalScrollEnabled */,
|
||||
!_vertical /* horizontalScrollEnabled */,
|
||||
true /* momentumEnabled */, () {
|
||||
num width = _layout.getWidth(_viewLength);
|
||||
num height = _layout.getHeight(_viewLength);
|
||||
width = width != null ? width : 0;
|
||||
height = height != null ? height : 0;
|
||||
return new Size(width, height);
|
||||
width = width ?? 0;
|
||||
height = height ?? 0;
|
||||
return Size(width, height);
|
||||
},
|
||||
_paginate && _snapToItems
|
||||
? Scroller.FAST_SNAP_DECELERATION_FACTOR
|
||||
|
@ -235,7 +230,7 @@ class GenericListView<D> extends View {
|
|||
scroller.onScrollerDragEnd.listen((e) => _decelStart());
|
||||
}
|
||||
if (_showScrollbar) {
|
||||
_scrollbar = new Scrollbar(scroller, true);
|
||||
_scrollbar = Scrollbar(scroller, true);
|
||||
}
|
||||
} else {
|
||||
_reserveArea();
|
||||
|
@ -245,6 +240,7 @@ class GenericListView<D> extends View {
|
|||
return node;
|
||||
}
|
||||
|
||||
@override
|
||||
void afterRender(Element node) {
|
||||
// If our data source is observable, observe it.
|
||||
if (_data is ObservableList<D>) {
|
||||
|
@ -300,6 +296,7 @@ class GenericListView<D> extends View {
|
|||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void enterDocument() {
|
||||
if (scroller != null) {
|
||||
onResize();
|
||||
|
@ -361,7 +358,7 @@ class GenericListView<D> extends View {
|
|||
_removeView(i);
|
||||
}
|
||||
_itemViews.clear();
|
||||
_activeInterval = new Interval(0, 0);
|
||||
_activeInterval = Interval(0, 0);
|
||||
if (scroller == null) {
|
||||
_reserveArea();
|
||||
}
|
||||
|
@ -381,9 +378,7 @@ class GenericListView<D> extends View {
|
|||
: scroller.getHorizontalOffset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates visible interval, based on the scroller position.
|
||||
*/
|
||||
/// Calculates visible interval, based on the scroller position.
|
||||
Interval getVisibleInterval() {
|
||||
return _layout.computeVisibleInterval(_offset, _viewLength, 0);
|
||||
}
|
||||
|
@ -394,14 +389,14 @@ class GenericListView<D> extends View {
|
|||
targetInterval = getVisibleInterval();
|
||||
} else {
|
||||
// If the view is not scrollable, render all elements.
|
||||
targetInterval = new Interval(0, _data.length);
|
||||
targetInterval = Interval(0, _data.length);
|
||||
}
|
||||
|
||||
if (_pages != null) {
|
||||
_pages.current.value = _layout.getPage(targetInterval.start, _viewLength);
|
||||
}
|
||||
if (_pages != null) {
|
||||
_pages.length.value = _data.length > 0
|
||||
_pages.length.value = _data.isNotEmpty
|
||||
? _layout.getPage(_data.length - 1, _viewLength) + 1
|
||||
: 0;
|
||||
}
|
||||
|
@ -450,7 +445,7 @@ class GenericListView<D> extends View {
|
|||
void _removeView(int index) {
|
||||
// Do not remove placeholder views as they need to stay present in case
|
||||
// they scroll out of view and then back into view.
|
||||
if (!(_itemViews[index] is _PlaceholderView)) {
|
||||
if (_itemViews[index] is! _PlaceholderView) {
|
||||
// Remove from the active DOM but don't destroy.
|
||||
_itemViews[index].node.remove();
|
||||
childViewRemoved(_itemViews[index]);
|
||||
|
@ -495,10 +490,8 @@ class GenericListView<D> extends View {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach a subview from the view replacing it with an empty placeholder view.
|
||||
* The detached subview can be safely reparented.
|
||||
*/
|
||||
/// Detach a subview from the view replacing it with an empty placeholder view.
|
||||
/// The detached subview can be safely reparented.
|
||||
View detachSubview(D itemData) {
|
||||
int index = findIndex(itemData);
|
||||
View view = _itemViews[index];
|
||||
|
@ -509,22 +502,20 @@ class GenericListView<D> extends View {
|
|||
_addView(index);
|
||||
view = _itemViews[index];
|
||||
}
|
||||
final placeholder = new _PlaceholderView();
|
||||
final placeholder = _PlaceholderView();
|
||||
view.node.replaceWith(placeholder.node);
|
||||
_itemViews[index] = placeholder;
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reattach a subview from the view that was detached from the view
|
||||
* by calling detachSubview. [callback] is called once the subview is
|
||||
* reattached and done animating into position.
|
||||
*/
|
||||
/// Reattach a subview from the view that was detached from the view
|
||||
/// by calling detachSubview. [callback] is called once the subview is
|
||||
/// reattached and done animating into position.
|
||||
void reattachSubview(D data, View view, bool animate) {
|
||||
int index = findIndex(data);
|
||||
// TODO(jacobr): perform some validation that the view is
|
||||
// really detached.
|
||||
var currentPosition;
|
||||
Coordinate currentPosition;
|
||||
if (animate) {
|
||||
currentPosition =
|
||||
FxUtil.computeRelativePosition(view.node, _containerElem);
|
||||
|
@ -614,14 +605,16 @@ class GenericListView<D> extends View {
|
|||
class FixedSizeListViewLayout<D> implements ListViewLayout<D> {
|
||||
final ViewFactory<D> itemViewFactory;
|
||||
final bool _vertical;
|
||||
List<D> _data;
|
||||
bool _paginate;
|
||||
final List<D> _data;
|
||||
final bool _paginate;
|
||||
|
||||
FixedSizeListViewLayout(
|
||||
this.itemViewFactory, this._data, this._vertical, this._paginate);
|
||||
|
||||
@override
|
||||
void onDataChange() {}
|
||||
|
||||
@override
|
||||
View newView(int index) {
|
||||
return itemViewFactory.newView(_data[index]);
|
||||
}
|
||||
|
@ -630,35 +623,41 @@ class FixedSizeListViewLayout<D> implements ListViewLayout<D> {
|
|||
return _vertical ? itemViewFactory.height : itemViewFactory.width;
|
||||
}
|
||||
|
||||
@override
|
||||
int getWidth(int viewLength) {
|
||||
return _vertical ? itemViewFactory.width : getLength(viewLength);
|
||||
}
|
||||
|
||||
@override
|
||||
int getHeight(int viewLength) {
|
||||
return _vertical ? getLength(viewLength) : itemViewFactory.height;
|
||||
}
|
||||
|
||||
@override
|
||||
int getEstimatedHeight(int viewLength) {
|
||||
// Returns the exact height as it is trivial to compute for this layout.
|
||||
return getHeight(viewLength);
|
||||
}
|
||||
|
||||
@override
|
||||
int getEstimatedWidth(int viewLength) {
|
||||
// Returns the exact height as it is trivial to compute for this layout.
|
||||
return getWidth(viewLength);
|
||||
}
|
||||
|
||||
@override
|
||||
int getEstimatedLength(int viewLength) {
|
||||
// Returns the exact length as it is trivial to compute for this layout.
|
||||
return getLength(viewLength);
|
||||
}
|
||||
|
||||
@override
|
||||
int getLength(int viewLength) {
|
||||
int itemLength = _vertical ? itemViewFactory.height : itemViewFactory.width;
|
||||
if (viewLength == null || viewLength == 0) {
|
||||
return itemLength * _data.length;
|
||||
} else if (_paginate) {
|
||||
if (_data.length > 0) {
|
||||
if (_data.isNotEmpty) {
|
||||
final pageLength = getPageLength(viewLength);
|
||||
return getPage(_data.length - 1, viewLength) * pageLength +
|
||||
Math.max(viewLength, pageLength);
|
||||
|
@ -670,6 +669,7 @@ class FixedSizeListViewLayout<D> implements ListViewLayout<D> {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int getOffset(int index) {
|
||||
return index * _itemLength;
|
||||
}
|
||||
|
@ -679,14 +679,17 @@ class FixedSizeListViewLayout<D> implements ListViewLayout<D> {
|
|||
return Math.max(1, itemsPerPage) * _itemLength;
|
||||
}
|
||||
|
||||
@override
|
||||
int getPage(int index, int viewLength) {
|
||||
return getOffset(index) ~/ getPageLength(viewLength);
|
||||
}
|
||||
|
||||
@override
|
||||
int getPageStartIndex(int page, int viewLength) {
|
||||
return getPageLength(viewLength) ~/ _itemLength * page;
|
||||
}
|
||||
|
||||
@override
|
||||
int getSnapIndex(num offset, num viewLength) {
|
||||
int index = (-offset / _itemLength).round();
|
||||
if (_paginate) {
|
||||
|
@ -695,6 +698,7 @@ class FixedSizeListViewLayout<D> implements ListViewLayout<D> {
|
|||
return GoogleMath.clamp(index, 0, _data.length - 1);
|
||||
}
|
||||
|
||||
@override
|
||||
Interval computeVisibleInterval(
|
||||
num offset, num viewLength, num bufferLength) {
|
||||
int targetIntervalStart =
|
||||
|
@ -703,28 +707,24 @@ class FixedSizeListViewLayout<D> implements ListViewLayout<D> {
|
|||
((-offset + viewLength + bufferLength) / _itemLength).ceil(),
|
||||
targetIntervalStart,
|
||||
_data.length);
|
||||
return new Interval(targetIntervalStart, targetIntervalEnd.toInt());
|
||||
return Interval(targetIntervalStart, targetIntervalEnd.toInt());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple list view class where each item has fixed width and height.
|
||||
*/
|
||||
/// Simple list view class where each item has fixed width and height.
|
||||
class ListView<D> extends GenericListView<D> {
|
||||
/**
|
||||
* Creates a new ListView for the given data. If [:_data:] is an
|
||||
* [:ObservableList<T>:] then it will listen to changes to the list and
|
||||
* update the view appropriately.
|
||||
*/
|
||||
/// Creates a new ListView for the given data. If [:_data:] is an
|
||||
/// [:ObservableList<T>:] then it will listen to changes to the list and
|
||||
/// update the view appropriately.
|
||||
ListView(List<D> data, ViewFactory<D> itemViewFactory, bool scrollable,
|
||||
bool vertical, ObservableValue<D> selectedItem,
|
||||
[bool snapToItems = false,
|
||||
bool paginate = false,
|
||||
bool removeClippedViews = false,
|
||||
bool showScrollbar = false,
|
||||
PageState pages = null])
|
||||
PageState pages])
|
||||
: super(
|
||||
new FixedSizeListViewLayout<D>(
|
||||
FixedSizeListViewLayout<D>(
|
||||
itemViewFactory, data, vertical, paginate),
|
||||
data,
|
||||
scrollable,
|
||||
|
@ -737,37 +737,38 @@ class ListView<D> extends GenericListView<D> {
|
|||
pages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Layout where each item may have variable size along the axis the list view
|
||||
* extends.
|
||||
*/
|
||||
/// Layout where each item may have variable size along the axis the list view
|
||||
/// extends.
|
||||
class VariableSizeListViewLayout<D> implements ListViewLayout<D> {
|
||||
List<D> _data;
|
||||
final List<D> _data;
|
||||
List<int> _itemOffsets;
|
||||
List<int> _lengths;
|
||||
int _lastOffset = 0;
|
||||
bool _vertical;
|
||||
bool _paginate;
|
||||
final bool _vertical;
|
||||
final bool _paginate;
|
||||
VariableSizeViewFactory<D> itemViewFactory;
|
||||
Interval _lastVisibleInterval;
|
||||
|
||||
VariableSizeListViewLayout(
|
||||
this.itemViewFactory, data, this._vertical, this._paginate)
|
||||
: _data = data,
|
||||
_lastVisibleInterval = new Interval(0, 0) {
|
||||
_lastVisibleInterval = Interval(0, 0) {
|
||||
_itemOffsets = <int>[];
|
||||
_lengths = <int>[];
|
||||
_itemOffsets.add(0);
|
||||
}
|
||||
|
||||
@override
|
||||
void onDataChange() {
|
||||
_itemOffsets.clear();
|
||||
_itemOffsets.add(0);
|
||||
_lengths.clear();
|
||||
}
|
||||
|
||||
@override
|
||||
View newView(int index) => itemViewFactory.newView(_data[index]);
|
||||
|
||||
@override
|
||||
int getWidth(int viewLength) {
|
||||
if (_vertical) {
|
||||
return itemViewFactory.getWidth(null);
|
||||
|
@ -776,6 +777,7 @@ class VariableSizeListViewLayout<D> implements ListViewLayout<D> {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int getHeight(int viewLength) {
|
||||
if (_vertical) {
|
||||
return getLength(viewLength);
|
||||
|
@ -784,6 +786,7 @@ class VariableSizeListViewLayout<D> implements ListViewLayout<D> {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int getEstimatedHeight(int viewLength) {
|
||||
if (_vertical) {
|
||||
return getEstimatedLength(viewLength);
|
||||
|
@ -792,6 +795,7 @@ class VariableSizeListViewLayout<D> implements ListViewLayout<D> {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int getEstimatedWidth(int viewLength) {
|
||||
if (_vertical) {
|
||||
return itemViewFactory.getWidth(null);
|
||||
|
@ -802,12 +806,13 @@ class VariableSizeListViewLayout<D> implements ListViewLayout<D> {
|
|||
|
||||
// TODO(jacobr): this logic is overly complicated. Replace with something
|
||||
// simpler.
|
||||
@override
|
||||
int getEstimatedLength(int viewLength) {
|
||||
if (_lengths.length == _data.length) {
|
||||
// No need to estimate... we have all the data already.
|
||||
return getLength(viewLength);
|
||||
}
|
||||
if (_itemOffsets.length > 1 && _lengths.length > 0) {
|
||||
if (_itemOffsets.length > 1 && _lengths.isNotEmpty) {
|
||||
// Estimate length by taking the average of the lengths
|
||||
// of the known views.
|
||||
num lengthFromAllButLastElement = 0;
|
||||
|
@ -828,8 +833,9 @@ class VariableSizeListViewLayout<D> implements ListViewLayout<D> {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int getLength(int viewLength) {
|
||||
if (_data.length == 0) {
|
||||
if (_data.isEmpty) {
|
||||
return viewLength;
|
||||
} else {
|
||||
// Hack so that _lengths[length - 1] is available.
|
||||
|
@ -839,6 +845,7 @@ class VariableSizeListViewLayout<D> implements ListViewLayout<D> {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
int getOffset(int index) {
|
||||
if (index >= _itemOffsets.length) {
|
||||
int offset = _itemOffsets[_itemOffsets.length - 1];
|
||||
|
@ -854,16 +861,19 @@ class VariableSizeListViewLayout<D> implements ListViewLayout<D> {
|
|||
return _itemOffsets[index];
|
||||
}
|
||||
|
||||
@override
|
||||
int getPage(int index, int viewLength) {
|
||||
// TODO(jacobr): implement.
|
||||
throw 'Not implemented';
|
||||
}
|
||||
|
||||
@override
|
||||
int getPageStartIndex(int page, int viewLength) {
|
||||
// TODO(jacobr): implement.
|
||||
throw 'Not implemented';
|
||||
}
|
||||
|
||||
@override
|
||||
int getSnapIndex(num offset, num viewLength) {
|
||||
for (int i = 1; i < _data.length; i++) {
|
||||
if (getOffset(i) + getOffset(i - 1) > -offset * 2) {
|
||||
|
@ -873,6 +883,7 @@ class VariableSizeListViewLayout<D> implements ListViewLayout<D> {
|
|||
return _data.length - 1;
|
||||
}
|
||||
|
||||
@override
|
||||
Interval computeVisibleInterval(
|
||||
num offset, num viewLength, num bufferLength) {
|
||||
offset = offset.toInt();
|
||||
|
@ -880,7 +891,7 @@ class VariableSizeListViewLayout<D> implements ListViewLayout<D> {
|
|||
_lastVisibleInterval != null ? _lastVisibleInterval.start : 0);
|
||||
int end = _findFirstItemAfter(-offset + viewLength + bufferLength,
|
||||
_lastVisibleInterval != null ? _lastVisibleInterval.end : 0);
|
||||
_lastVisibleInterval = new Interval(start, Math.max(start, end));
|
||||
_lastVisibleInterval = Interval(start, Math.max(start, end));
|
||||
_lastOffset = offset;
|
||||
return _lastVisibleInterval;
|
||||
}
|
||||
|
@ -914,9 +925,9 @@ class VariableSizeListView<D> extends GenericListView<D> {
|
|||
bool paginate = false,
|
||||
bool removeClippedViews = false,
|
||||
bool showScrollbar = false,
|
||||
PageState pages = null])
|
||||
PageState pages])
|
||||
: super(
|
||||
new VariableSizeListViewLayout(
|
||||
VariableSizeListViewLayout(
|
||||
itemViewFactory, data, vertical, paginate),
|
||||
data,
|
||||
scrollable,
|
||||
|
@ -929,19 +940,21 @@ class VariableSizeListView<D> extends GenericListView<D> {
|
|||
pages);
|
||||
}
|
||||
|
||||
/** A back button that is equivalent to clicking "back" in the browser. */
|
||||
/// A back button that is equivalent to clicking "back" in the browser. */
|
||||
class BackButton extends View {
|
||||
BackButton();
|
||||
|
||||
Element render() => new Element.html('<div class="back-arrow button"></div>');
|
||||
@override
|
||||
Element render() => Element.html('<div class="back-arrow button"></div>');
|
||||
|
||||
@override
|
||||
void afterRender(Element node) {
|
||||
addOnClick((e) => window.history.back());
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(terry): Maybe should be part of ButtonView class in appstack/view?
|
||||
/** OS button. */
|
||||
/// OS button. */
|
||||
class PushButtonView extends View {
|
||||
final String _text;
|
||||
final String _cssClass;
|
||||
|
@ -949,10 +962,12 @@ class PushButtonView extends View {
|
|||
|
||||
PushButtonView(this._text, this._cssClass, this._clickHandler);
|
||||
|
||||
@override
|
||||
Element render() {
|
||||
return new Element.html('<button class="${_cssClass}">${_text}</button>');
|
||||
return Element.html('<button class="$_cssClass">$_text</button>');
|
||||
}
|
||||
|
||||
@override
|
||||
void afterRender(Element node) {
|
||||
addOnClick(_clickHandler);
|
||||
}
|
||||
|
@ -961,7 +976,7 @@ class PushButtonView extends View {
|
|||
// TODO(terry): Add a drop shadow around edge and corners need to be rounded.
|
||||
// Need to support conveyor for contents of dialog so it's not
|
||||
// larger than the parent window.
|
||||
/** A generic dialog view supports title, done button and dialog content. */
|
||||
/// A generic dialog view supports title, done button and dialog content. */
|
||||
class DialogView extends View {
|
||||
final String _title;
|
||||
final String _cssName;
|
||||
|
@ -971,8 +986,9 @@ class DialogView extends View {
|
|||
|
||||
DialogView(this._title, this._cssName, this._content);
|
||||
|
||||
@override
|
||||
Element render() {
|
||||
final node = new Element.html('''
|
||||
final node = Element.html('''
|
||||
<div class="dialog-modal">
|
||||
<div class="dialog $_cssName">
|
||||
<div class="dialog-title-area">
|
||||
|
@ -982,8 +998,8 @@ class DialogView extends View {
|
|||
</div>
|
||||
</div>''');
|
||||
|
||||
_done = new PushButtonView(
|
||||
'Done', 'done-button', EventBatch.wrap((e) => onDone()));
|
||||
_done =
|
||||
PushButtonView('Done', 'done-button', EventBatch.wrap((e) => onDone()));
|
||||
final titleArea = node.querySelector('.dialog-title-area');
|
||||
titleArea.nodes.add(_done.node);
|
||||
|
||||
|
@ -993,6 +1009,6 @@ class DialogView extends View {
|
|||
return node;
|
||||
}
|
||||
|
||||
/** Override to handle dialog done. */
|
||||
/// Override to handle dialog done. */
|
||||
void onDone() {}
|
||||
}
|
||||
|
|
6
samples-dev/swarm/analysis_options.yaml
Normal file
6
samples-dev/swarm/analysis_options.yaml
Normal file
|
@ -0,0 +1,6 @@
|
|||
include: package:lints/recommended.yaml
|
||||
|
||||
analyzer:
|
||||
exclude: [build/**]
|
||||
language:
|
||||
|
5
samples-dev/swarm/pubspec.yaml
Normal file
5
samples-dev/swarm/pubspec.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
name: swarm
|
||||
version: 0.0.1
|
||||
environment:
|
||||
sdk: '>=2.17.0 <3.0.0'
|
||||
dev_dependencies: {lints: ^2.0.0}
|
|
@ -9,5 +9,5 @@ library swarm;
|
|||
import 'swarmlib.dart';
|
||||
|
||||
void main() {
|
||||
new Swarm().run();
|
||||
Swarm().run();
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
part of base;
|
||||
|
||||
typedef void AnimationCallback(num currentTime);
|
||||
typedef AnimationCallback = void Function(num currentTime);
|
||||
|
||||
class CallbackData {
|
||||
final AnimationCallback callback;
|
||||
|
@ -13,28 +13,26 @@ class CallbackData {
|
|||
|
||||
static int _nextId = 1;
|
||||
|
||||
bool ready(num time) => minTime == null || minTime <= time;
|
||||
bool ready(num time) => minTime <= time;
|
||||
|
||||
CallbackData(this.callback, this.minTime) : id = _nextId++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Animation scheduler implementing the functionality provided by
|
||||
* [:window.requestAnimationFrame:] for platforms that do not support it
|
||||
* or support it badly. When multiple UI components are animating at once,
|
||||
* this approach yields superior performance to calling setTimeout/Timer
|
||||
* directly as all pieces of the UI will animate at the same time resulting in
|
||||
* fewer layouts.
|
||||
*/
|
||||
/// Animation scheduler implementing the functionality provided by
|
||||
/// [:window.requestAnimationFrame:] for platforms that do not support it
|
||||
/// or support it badly. When multiple UI components are animating at once,
|
||||
/// this approach yields superior performance to calling setTimeout/Timer
|
||||
/// directly as all pieces of the UI will animate at the same time resulting in
|
||||
/// fewer layouts.
|
||||
// TODO(jacobr): use window.requestAnimationFrame when it is available and
|
||||
// 60fps for the current browser.
|
||||
class AnimationScheduler {
|
||||
static const FRAMES_PER_SECOND = 60;
|
||||
static const MS_PER_FRAME = 1000 ~/ FRAMES_PER_SECOND;
|
||||
|
||||
/** List of callbacks to be executed next animation frame. */
|
||||
/// List of callbacks to be executed next animation frame. */
|
||||
List<CallbackData> _callbacks;
|
||||
bool _isMobileSafari = false;
|
||||
final bool _isMobileSafari = false;
|
||||
late CssStyleDeclaration _safariHackStyle;
|
||||
int _frameCount = 0;
|
||||
|
||||
|
@ -49,23 +47,19 @@ class AnimationScheduler {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the pending callback matching the specified [id].
|
||||
* This is not heavily optimized as typically users don't cancel animation
|
||||
* frames.
|
||||
*/
|
||||
/// Cancel the pending callback matching the specified [id].
|
||||
/// This is not heavily optimized as typically users don't cancel animation
|
||||
/// frames.
|
||||
void cancelRequestAnimationFrame(int id) {
|
||||
_callbacks = _callbacks.where((CallbackData e) => e.id != id).toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule [callback] to execute at the next animation frame that occurs
|
||||
* at or after [minTime]. If [minTime] is not specified, the first available
|
||||
* animation frame is used. Returns an id that can be used to cancel the
|
||||
* pending callback.
|
||||
*/
|
||||
/// Schedule [callback] to execute at the next animation frame that occurs
|
||||
/// at or after [minTime]. If [minTime] is not specified, the first available
|
||||
/// animation frame is used. Returns an id that can be used to cancel the
|
||||
/// pending callback.
|
||||
int requestAnimationFrame(AnimationCallback callback,
|
||||
[Element? element = null, num? minTime = null]) {
|
||||
[Element? element, num? minTime]) {
|
||||
final callbackData = CallbackData(callback, minTime!);
|
||||
_requestAnimationFrameHelper(callbackData);
|
||||
return callbackData.id;
|
||||
|
@ -114,7 +108,7 @@ class AnimationScheduler {
|
|||
(callbackData.callback)(minTime);
|
||||
} catch (e) {
|
||||
final msg = e.toString();
|
||||
print('Suppressed exception ${msg} triggered by callback');
|
||||
print('Suppressed exception $msg triggered by callback');
|
||||
}
|
||||
} else {
|
||||
_callbacks.add(callbackData);
|
||||
|
|
|
@ -6,72 +6,48 @@ part of base;
|
|||
|
||||
// TODO(jacobr): cache these results.
|
||||
// TODO(jacobr): figure out how to test this.
|
||||
/**
|
||||
* Utils for device detection.
|
||||
*/
|
||||
/// Utils for device detection.
|
||||
class Device {
|
||||
/**
|
||||
* The regular expression for detecting an iPhone or iPod.
|
||||
*/
|
||||
/// The regular expression for detecting an iPhone or iPod.
|
||||
static final _IPHONE_REGEX = RegExp('iPhone|iPod');
|
||||
|
||||
/**
|
||||
* The regular expression for detecting an iPhone or iPod or iPad.
|
||||
*/
|
||||
/// The regular expression for detecting an iPhone or iPod or iPad.
|
||||
static final _MOBILE_SAFARI_REGEX = RegExp('iPhone|iPod|iPad');
|
||||
|
||||
/**
|
||||
* The regular expression for detecting an iPhone or iPod or iPad simulator.
|
||||
*/
|
||||
/// The regular expression for detecting an iPhone or iPod or iPad simulator.
|
||||
static final _APPLE_SIM_REGEX = RegExp('iP.*Simulator');
|
||||
|
||||
/**
|
||||
* Gets the browser's user agent. Using this function allows tests to inject
|
||||
* the user agent.
|
||||
* Returns the user agent.
|
||||
*/
|
||||
/// Gets the browser's user agent. Using this function allows tests to inject
|
||||
/// the user agent.
|
||||
/// Returns the user agent.
|
||||
static String get userAgent => window.navigator.userAgent;
|
||||
|
||||
/**
|
||||
* Determines if the current device is an iPhone or iPod.
|
||||
* Returns true if the current device is an iPhone or iPod.
|
||||
*/
|
||||
/// Determines if the current device is an iPhone or iPod.
|
||||
/// Returns true if the current device is an iPhone or iPod.
|
||||
static bool get isIPhone => _IPHONE_REGEX.hasMatch(userAgent);
|
||||
|
||||
/**
|
||||
* Determines if the current device is an iPad.
|
||||
* Returns true if the current device is an iPad.
|
||||
*/
|
||||
/// Determines if the current device is an iPad.
|
||||
/// Returns true if the current device is an iPad.
|
||||
static bool get isIPad => userAgent.contains("iPad", 0);
|
||||
|
||||
/**
|
||||
* Determines if the current device is running Firefox.
|
||||
*/
|
||||
/// Determines if the current device is running Firefox.
|
||||
static bool get isFirefox => userAgent.contains("Firefox", 0);
|
||||
|
||||
/**
|
||||
* Determines if the current device is an iPhone or iPod or iPad.
|
||||
* Returns true if the current device is an iPhone or iPod or iPad.
|
||||
*/
|
||||
/// Determines if the current device is an iPhone or iPod or iPad.
|
||||
/// Returns true if the current device is an iPhone or iPod or iPad.
|
||||
static bool get isMobileSafari => _MOBILE_SAFARI_REGEX.hasMatch(userAgent);
|
||||
|
||||
/**
|
||||
* Determines if the current device is the iP* Simulator.
|
||||
* Returns true if the current device is an iP* Simulator.
|
||||
*/
|
||||
/// Determines if the current device is the iP* Simulator.
|
||||
/// Returns true if the current device is an iP* Simulator.
|
||||
static bool get isAppleSimulator => _APPLE_SIM_REGEX.hasMatch(userAgent);
|
||||
|
||||
/**
|
||||
* Determines if the current device is an Android.
|
||||
* Returns true if the current device is an Android.
|
||||
*/
|
||||
/// Determines if the current device is an Android.
|
||||
/// Returns true if the current device is an Android.
|
||||
static bool get isAndroid => userAgent.contains("Android", 0);
|
||||
|
||||
/**
|
||||
* Determines if the current device is WebOS WebKit.
|
||||
* Returns true if the current device is WebOS WebKit.
|
||||
*/
|
||||
/// Determines if the current device is WebOS WebKit.
|
||||
/// Returns true if the current device is WebOS WebKit.
|
||||
static bool get isWebOs => userAgent.contains("webOS", 0);
|
||||
|
||||
static late bool supportsTouch = isMobileSafari || isAndroid;
|
||||
static bool supportsTouch = isMobileSafari || isAndroid;
|
||||
}
|
||||
|
|
|
@ -4,11 +4,9 @@
|
|||
|
||||
part of base;
|
||||
|
||||
/**
|
||||
* Embedded DSL for generating DOM elements.
|
||||
*/
|
||||
/// Embedded DSL for generating DOM elements.
|
||||
class Dom {
|
||||
static void ready(void f()) {
|
||||
static void ready(void Function() f) {
|
||||
if (document.readyState == 'interactive' ||
|
||||
document.readyState == 'complete') {
|
||||
Timer.run(f);
|
||||
|
@ -20,7 +18,7 @@ class Dom {
|
|||
}
|
||||
}
|
||||
|
||||
/** Adds the given <style> text to the page. */
|
||||
/// Adds the given <style> text to the page. */
|
||||
static void addStyle(String cssText) {
|
||||
var style = Element.tag('style') as StyleElement;
|
||||
style.type = 'text/css';
|
||||
|
|
|
@ -4,34 +4,28 @@
|
|||
|
||||
part of base;
|
||||
|
||||
/**
|
||||
* This class has static fields that hold objects that this isolate
|
||||
* uses to interact with the environment. Which objects are available
|
||||
* depend on how the isolate was started. In the UI isolate
|
||||
* of the browser, the window object is available.
|
||||
*/
|
||||
/// This class has static fields that hold objects that this isolate
|
||||
/// uses to interact with the environment. Which objects are available
|
||||
/// depend on how the isolate was started. In the UI isolate
|
||||
/// of the browser, the window object is available.
|
||||
class Env {
|
||||
static AnimationScheduler _animationScheduler = AnimationScheduler();
|
||||
static final AnimationScheduler _animationScheduler = AnimationScheduler();
|
||||
|
||||
/**
|
||||
* Provides functionality similar to [:window.requestAnimationFrame:] for
|
||||
* all platforms. [callback] is executed on the next animation frame that
|
||||
* occurs at or after [minTime]. If [minTime] is not specified, the first
|
||||
* available animation frame is used. Returns an id that can be used to
|
||||
* cancel the pending callback.
|
||||
*/
|
||||
/// Provides functionality similar to [:window.requestAnimationFrame:] for
|
||||
/// all platforms. [callback] is executed on the next animation frame that
|
||||
/// occurs at or after [minTime]. If [minTime] is not specified, the first
|
||||
/// available animation frame is used. Returns an id that can be used to
|
||||
/// cancel the pending callback.
|
||||
static int requestAnimationFrame(AnimationCallback callback,
|
||||
[Element? element = null, num? minTime = null]) {
|
||||
[Element? element, num? minTime]) {
|
||||
return _animationScheduler.requestAnimationFrame(
|
||||
callback, element, minTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the pending callback callback matching the specified [id].
|
||||
*/
|
||||
/// Cancel the pending callback callback matching the specified [id].
|
||||
static void cancelRequestAnimationFrame(int id) {
|
||||
_animationScheduler.cancelRequestAnimationFrame(id);
|
||||
}
|
||||
}
|
||||
|
||||
typedef void XMLHttpRequestCompleted(HttpRequest req);
|
||||
typedef XMLHttpRequestCompleted = void Function(HttpRequest req);
|
||||
|
|
|
@ -4,127 +4,101 @@
|
|||
|
||||
part of base;
|
||||
|
||||
/**
|
||||
* A utility class for representing two-dimensional sizes.
|
||||
*/
|
||||
/// A utility class for representing two-dimensional sizes.
|
||||
class Size {
|
||||
num width;
|
||||
num height;
|
||||
|
||||
Size(num this.width, num this.height) {}
|
||||
Size(this.width, this.height);
|
||||
|
||||
@override
|
||||
bool operator ==(covariant Size other) {
|
||||
return other != null && width == other.width && height == other.height;
|
||||
return width == other.width && height == other.height;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => throw UnimplementedError();
|
||||
|
||||
/**
|
||||
* Returns the area of the size (width * height).
|
||||
*/
|
||||
/// Returns the area of the size (width * height).
|
||||
num area() {
|
||||
return width * height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ratio of the size's width to its height.
|
||||
*/
|
||||
/// Returns the ratio of the size's width to its height.
|
||||
num aspectRatio() {
|
||||
return width / height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamps the width and height parameters upward to integer values.
|
||||
* Returns this size with ceil'd components.
|
||||
*/
|
||||
/// Clamps the width and height parameters upward to integer values.
|
||||
/// Returns this size with ceil'd components.
|
||||
Size ceil() {
|
||||
width = width.ceil();
|
||||
height = height.ceil();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of the Size.
|
||||
*/
|
||||
/// Returns a copy of the Size.
|
||||
Size clone() {
|
||||
return Size(width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this Size is the same size or smaller than the
|
||||
* [target] size in both dimensions.
|
||||
*/
|
||||
/// Returns true if this Size is the same size or smaller than the
|
||||
/// [target] size in both dimensions.
|
||||
bool fitsInside(Size target) {
|
||||
return width <= target.width && height <= target.height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamps the width and height parameters downward to integer values.
|
||||
* Returns this size with floored components.
|
||||
*/
|
||||
/// Clamps the width and height parameters downward to integer values.
|
||||
/// Returns this size with floored components.
|
||||
Size floor() {
|
||||
width = width.floor();
|
||||
height = height.floor();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the longer of the two dimensions in the size.
|
||||
*/
|
||||
/// Returns the longer of the two dimensions in the size.
|
||||
num getLongest() {
|
||||
return max(width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the shorter of the two dimensions in the size.
|
||||
*/
|
||||
/// Returns the shorter of the two dimensions in the size.
|
||||
num getShortest() {
|
||||
return min(width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the size has zero area, false if both dimensions
|
||||
* are non-zero numbers.
|
||||
*/
|
||||
/// Returns true if the size has zero area, false if both dimensions
|
||||
/// are non-zero numbers.
|
||||
bool get isEmpty {
|
||||
return area() == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the perimeter of the size (width + height) * 2.
|
||||
*/
|
||||
/// Returns the perimeter of the size (width + height) * 2.
|
||||
num perimeter() {
|
||||
return (width + height) * 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds the width and height parameters to integer values.
|
||||
* Returns this size with rounded components.
|
||||
*/
|
||||
/// Rounds the width and height parameters to integer values.
|
||||
/// Returns this size with rounded components.
|
||||
Size round() {
|
||||
width = width.round();
|
||||
height = height.round();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scales the size uniformly by a factor.
|
||||
* [s] The scale factor.
|
||||
* Returns this Size object after scaling.
|
||||
*/
|
||||
/// Scales the size uniformly by a factor.
|
||||
/// [s] The scale factor.
|
||||
/// Returns this Size object after scaling.
|
||||
Size scale(num s) {
|
||||
width *= s;
|
||||
height *= s;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uniformly scales the size to fit inside the dimensions of a given size. The
|
||||
* original aspect ratio will be preserved.
|
||||
*
|
||||
* This function assumes that both Sizes contain strictly positive dimensions.
|
||||
* Returns this Size object, after optional scaling.
|
||||
*/
|
||||
/// Uniformly scales the size to fit inside the dimensions of a given size. The
|
||||
/// original aspect ratio will be preserved.
|
||||
///
|
||||
/// This function assumes that both Sizes contain strictly positive dimensions.
|
||||
/// Returns this Size object, after optional scaling.
|
||||
Size scaleToFit(Size target) {
|
||||
num s = aspectRatio() > target.aspectRatio()
|
||||
? target.width / width
|
||||
|
@ -132,11 +106,10 @@ class Size {
|
|||
return scale(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a nice string representing size.
|
||||
* Returns in the form (50 x 73).
|
||||
*/
|
||||
/// Returns a nice string representing size.
|
||||
/// Returns in the form (50 x 73).
|
||||
@override
|
||||
String toString() {
|
||||
return "(${width} x ${height})";
|
||||
return "($width x $height)";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,14 +4,12 @@
|
|||
|
||||
part of layout;
|
||||
|
||||
/**
|
||||
* Implements a grid-based layout system based on:
|
||||
* [http://dev.w3.org/csswg/css3-grid-align/]
|
||||
*
|
||||
* This layout is designed to support animations and work on browsers that
|
||||
* don't support grid natively. As such, we implement it on top of absolute
|
||||
* positioning.
|
||||
*/
|
||||
/// Implements a grid-based layout system based on:
|
||||
/// [http://dev.w3.org/csswg/css3-grid-align/]
|
||||
///
|
||||
/// This layout is designed to support animations and work on browsers that
|
||||
/// don't support grid natively. As such, we implement it on top of absolute
|
||||
/// positioning.
|
||||
// TODO(jmesserly): the DOM integration still needs work:
|
||||
// - The grid assumes it is absolutely positioned in its container.
|
||||
// Because of that, the grid doesn't work right unless it has at least one
|
||||
|
@ -43,31 +41,27 @@ part of layout;
|
|||
// - Optimize for the case of no content sized tracks
|
||||
// - Optimize for the "incremental update" cases
|
||||
class GridLayout extends ViewLayout {
|
||||
/** Configuration parameters defined in CSS. */
|
||||
/// Configuration parameters defined in CSS. */
|
||||
final GridTrackList? rows;
|
||||
final GridTrackList? columns;
|
||||
final GridTemplate? template;
|
||||
|
||||
/** The default sizing for new rows. */
|
||||
/// The default sizing for new rows. */
|
||||
final TrackSizing rowSizing;
|
||||
|
||||
/** The default sizing for new columns. */
|
||||
/// The default sizing for new columns. */
|
||||
final TrackSizing columnSizing;
|
||||
|
||||
/**
|
||||
* This stores the grid's size during a layout.
|
||||
* Used for rows/columns with % or fr units.
|
||||
*/
|
||||
/// This stores the grid's size during a layout.
|
||||
/// Used for rows/columns with % or fr units.
|
||||
int? _gridWidth, _gridHeight;
|
||||
|
||||
/**
|
||||
* During a layout, this stores all row/column size information.
|
||||
* Because grid-items can implicitly specify their own rows/columns, we can't
|
||||
* compute this until we know the set of items.
|
||||
*/
|
||||
/// During a layout, this stores all row/column size information.
|
||||
/// Because grid-items can implicitly specify their own rows/columns, we can't
|
||||
/// compute this until we know the set of items.
|
||||
late List<GridTrack> _rowTracks, _columnTracks;
|
||||
|
||||
/** During a layout, tracks which dimension we're processing. */
|
||||
/// During a layout, tracks which dimension we're processing. */
|
||||
Dimension? _dimension;
|
||||
|
||||
GridLayout(Positionable view)
|
||||
|
@ -83,9 +77,12 @@ class GridLayout extends ViewLayout {
|
|||
_columnTracks = columns?.tracks ?? [];
|
||||
}
|
||||
|
||||
@override
|
||||
int? get currentWidth => _gridWidth;
|
||||
@override
|
||||
int? get currentHeight => _gridHeight;
|
||||
|
||||
@override
|
||||
void cacheExistingBrowserLayout() {
|
||||
// We don't need to do anything as we don't rely on the _cachedViewRect
|
||||
// when the grid layout is used.
|
||||
|
@ -93,14 +90,15 @@ class GridLayout extends ViewLayout {
|
|||
|
||||
// TODO(jacobr): cleanup this method so that it returns a Future
|
||||
// rather than taking a Completer as an argument.
|
||||
/** The main entry point for layout computation. */
|
||||
/// The main entry point for layout computation. */
|
||||
@override
|
||||
void measureLayout(Future<Size> size, Completer<bool>? changed) {
|
||||
_ensureAllTracks();
|
||||
size.then((value) {
|
||||
_gridWidth = value.width as int?;
|
||||
_gridHeight = value.height as int?;
|
||||
|
||||
if (_rowTracks.length > 0 && _columnTracks.length > 0) {
|
||||
if (_rowTracks.isNotEmpty && _columnTracks.isNotEmpty) {
|
||||
_measureTracks();
|
||||
_setBoundsOfChildren();
|
||||
if (changed != null) {
|
||||
|
@ -110,10 +108,8 @@ class GridLayout extends ViewLayout {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* The top level measurement function.
|
||||
* [http://dev.w3.org/csswg/css3-grid-align/#calculating-size-of-grid-tracks]
|
||||
*/
|
||||
/// The top level measurement function.
|
||||
/// [http://dev.w3.org/csswg/css3-grid-align/#calculating-size-of-grid-tracks]
|
||||
void _measureTracks() {
|
||||
// Resolve logical width, then height. Width comes first so we can use
|
||||
// the width when determining the content-sized height.
|
||||
|
@ -137,14 +133,12 @@ class GridLayout extends ViewLayout {
|
|||
return Math.max(0, remaining);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the core Grid Track sizing algorithm. It is run for Grid columns
|
||||
* and Grid rows. The goal of the function is to ensure:
|
||||
* 1. That each Grid Track satisfies its minSizing
|
||||
* 2. That each Grid Track grows from the breadth which satisfied its
|
||||
* minSizing to a breadth which satifies its
|
||||
* maxSizing, subject to RemainingSpace.
|
||||
*/
|
||||
/// This is the core Grid Track sizing algorithm. It is run for Grid columns
|
||||
/// and Grid rows. The goal of the function is to ensure:
|
||||
/// 1. That each Grid Track satisfies its minSizing
|
||||
/// 2. That each Grid Track grows from the breadth which satisfied its
|
||||
/// minSizing to a breadth which satifies its
|
||||
/// maxSizing, subject to RemainingSpace.
|
||||
// Note: spec does not correctly doc all the parameters to this function.
|
||||
void _computeUsedBreadthOfTracks(List<GridTrack> tracks) {
|
||||
// TODO(jmesserly): as a performance optimization we could cache this
|
||||
|
@ -161,12 +155,12 @@ class GridLayout extends ViewLayout {
|
|||
}
|
||||
|
||||
// 2. Resolve content-based MinTrackSizingFunctions
|
||||
final USED_BREADTH = const _UsedBreadthAccumulator();
|
||||
final MAX_BREADTH = const _MaxBreadthAccumulator();
|
||||
final USEDBREADTH = const _UsedBreadthAccumulator();
|
||||
final MAXBREADTH = const _MaxBreadthAccumulator();
|
||||
|
||||
_distributeSpaceBySpanCount(items, ContentSizeMode.MIN, USED_BREADTH);
|
||||
_distributeSpaceBySpanCount(items, ContentSizeMode.MIN, USEDBREADTH);
|
||||
|
||||
_distributeSpaceBySpanCount(items, ContentSizeMode.MAX, USED_BREADTH);
|
||||
_distributeSpaceBySpanCount(items, ContentSizeMode.MAX, USEDBREADTH);
|
||||
|
||||
// 3. Ensure that maxBreadth is as big as usedBreadth for each track
|
||||
for (final t in tracks) {
|
||||
|
@ -176,16 +170,16 @@ class GridLayout extends ViewLayout {
|
|||
}
|
||||
|
||||
// 4. Resolve content-based MaxTrackSizingFunctions
|
||||
_distributeSpaceBySpanCount(items, ContentSizeMode.MIN, MAX_BREADTH);
|
||||
_distributeSpaceBySpanCount(items, ContentSizeMode.MIN, MAXBREADTH);
|
||||
|
||||
_distributeSpaceBySpanCount(items, ContentSizeMode.MAX, MAX_BREADTH);
|
||||
_distributeSpaceBySpanCount(items, ContentSizeMode.MAX, MAXBREADTH);
|
||||
|
||||
// 5. Grow all Grid Tracks in GridTracks from their usedBreadth up to their
|
||||
// maxBreadth value until RemainingSpace is exhausted.
|
||||
// Note: it's not spec'd what to pass as the accumulator, but usedBreadth
|
||||
// seems right.
|
||||
_distributeSpaceToTracks(
|
||||
tracks, _getRemainingSpace(tracks), USED_BREADTH, false);
|
||||
tracks, _getRemainingSpace(tracks), USEDBREADTH, false);
|
||||
|
||||
// Spec wording is confusing about which direction this assignment happens,
|
||||
// but this is the way that makes sense.
|
||||
|
@ -203,10 +197,8 @@ class GridLayout extends ViewLayout {
|
|||
_computeTrackPositions(tracks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Final steps to finish positioning tracks. Takes the track size and uses
|
||||
* it to get start and end positions. Also rounds the positions to integers.
|
||||
*/
|
||||
/// Final steps to finish positioning tracks. Takes the track size and uses
|
||||
/// it to get start and end positions. Also rounds the positions to integers.
|
||||
void _computeTrackPositions(List<GridTrack> tracks) {
|
||||
// Compute start positions of tracks, as well as the final position
|
||||
|
||||
|
@ -238,14 +230,12 @@ class GridLayout extends ViewLayout {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method computes a '1fr' value, referred to as the
|
||||
* tempBreadth, for a set of Grid Tracks. The value computed
|
||||
* will ensure that when the tempBreadth is multiplied by the
|
||||
* fractions associated with tracks, that the UsedBreadths of tracks
|
||||
* will increase by an amount equal to the maximum of zero and the specified
|
||||
* freeSpace less the sum of the current UsedBreadths.
|
||||
*/
|
||||
/// This method computes a '1fr' value, referred to as the
|
||||
/// tempBreadth, for a set of Grid Tracks. The value computed
|
||||
/// will ensure that when the tempBreadth is multiplied by the
|
||||
/// fractions associated with tracks, that the UsedBreadths of tracks
|
||||
/// will increase by an amount equal to the maximum of zero and the specified
|
||||
/// freeSpace less the sum of the current UsedBreadths.
|
||||
num _calcNormalizedFractionBreadth(List<GridTrack> tracks) {
|
||||
final fractionTracks = tracks.where((t) => t.maxSizing.isFraction).toList();
|
||||
|
||||
|
@ -279,11 +269,9 @@ class GridLayout extends ViewLayout {
|
|||
return spaceNeededFromFractionTracks / accumulatedFractions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that for each Grid Track in tracks, a value will be
|
||||
* computed, updatedBreadth, that represents the Grid Track's share of
|
||||
* freeSpace.
|
||||
*/
|
||||
/// Ensures that for each Grid Track in tracks, a value will be
|
||||
/// computed, updatedBreadth, that represents the Grid Track's share of
|
||||
/// freeSpace.
|
||||
void _distributeSpaceToTracks(List<GridTrack?> tracks, num? freeSpace,
|
||||
_BreadthAccumulator breadth, bool ignoreMaxBreadth) {
|
||||
// TODO(jmesserly): in some cases it would be safe to sort the passed in
|
||||
|
@ -321,14 +309,12 @@ class GridLayout extends ViewLayout {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function prioritizes the distribution of space driven by Grid Items
|
||||
* in content-sized Grid Tracks by the Grid Item's spanCount. That is, Grid
|
||||
* Items having a lower spanCount have an opportunity to increase the size of
|
||||
* the Grid Tracks they cover before those with larger SpanCounts.
|
||||
*
|
||||
* Note: items are assumed to be already sorted in increasing span count
|
||||
*/
|
||||
/// This function prioritizes the distribution of space driven by Grid Items
|
||||
/// in content-sized Grid Tracks by the Grid Item's spanCount. That is, Grid
|
||||
/// Items having a lower spanCount have an opportunity to increase the size of
|
||||
/// the Grid Tracks they cover before those with larger SpanCounts.
|
||||
///
|
||||
/// Note: items are assumed to be already sorted in increasing span count
|
||||
void _distributeSpaceBySpanCount(List<ViewLayout> items,
|
||||
ContentSizeMode sizeMode, _BreadthAccumulator breadth) {
|
||||
items = items
|
||||
|
@ -366,10 +352,8 @@ class GridLayout extends ViewLayout {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if we have an appropriate content sized dimension, and don't
|
||||
* cross a fractional track.
|
||||
*/
|
||||
/// Returns true if we have an appropriate content sized dimension, and don't
|
||||
/// cross a fractional track.
|
||||
static bool _hasContentSizedTracks(Iterable<GridTrack?> tracks,
|
||||
ContentSizeMode sizeMode, _BreadthAccumulator breadth) {
|
||||
for (final t in tracks) {
|
||||
|
@ -383,7 +367,7 @@ class GridLayout extends ViewLayout {
|
|||
return false;
|
||||
}
|
||||
|
||||
/** Ensures that the numbered track exists. */
|
||||
/// Ensures that the numbered track exists. */
|
||||
void _ensureTrack(
|
||||
List<GridTrack> tracks, TrackSizing sizing, int start, int span) {
|
||||
// Start is 1-based. Make it 0-based.
|
||||
|
@ -402,13 +386,11 @@ class GridLayout extends ViewLayout {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans children creating GridLayoutParams as needed, and creates all of the
|
||||
* rows and columns that we will need.
|
||||
*
|
||||
* Note: this can potentially create new qrows/columns, so this needs to be
|
||||
* run before the track sizing algorithm.
|
||||
*/
|
||||
/// Scans children creating GridLayoutParams as needed, and creates all of the
|
||||
/// rows and columns that we will need.
|
||||
///
|
||||
/// Note: this can potentially create new qrows/columns, so this needs to be
|
||||
/// run before the track sizing algorithm.
|
||||
void _ensureAllTracks() {
|
||||
final items = view.childViews.map((view_) => view_.layout);
|
||||
|
||||
|
@ -423,9 +405,7 @@ class GridLayout extends ViewLayout {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the track sizes that were computed, position children in the grid.
|
||||
*/
|
||||
/// Given the track sizes that were computed, position children in the grid.
|
||||
void _setBoundsOfChildren() {
|
||||
final items = view.childViews.map((view_) => view_.layout);
|
||||
|
||||
|
@ -453,6 +433,7 @@ class GridLayout extends ViewLayout {
|
|||
} else if (_dimension == Dimension.HEIGHT) {
|
||||
return _gridHeight;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
_GridLocation _getTrackLocationX(GridLayoutParams childLayout) {
|
||||
|
@ -475,7 +456,7 @@ class GridLayout extends ViewLayout {
|
|||
return _GridLocation(start, end - start);
|
||||
}
|
||||
|
||||
/** Gets the tracks that this item crosses. */
|
||||
/// Gets the tracks that this item crosses. */
|
||||
// TODO(jmesserly): might be better to return an iterable
|
||||
List<GridTrack?> _getTracks(ViewLayout item) {
|
||||
GridLayoutParams? childLayout = item.layoutParams as GridLayoutParams?;
|
||||
|
|
|
@ -4,23 +4,22 @@
|
|||
|
||||
part of layout;
|
||||
|
||||
/**
|
||||
* Caches the layout parameters that were specified in CSS during a layout
|
||||
* computation. These values are immutable during a layout.
|
||||
*/
|
||||
/// Caches the layout parameters that were specified in CSS during a layout
|
||||
/// computation. These values are immutable during a layout.
|
||||
// TODO(jmesserly): I would like all fields to be final, but it's too painful
|
||||
// to do this right now in Dart. If I create a factory constructor, then I need
|
||||
// to create locals, and pass all parameters to the real constructor. Each
|
||||
// field ends up being mentioned 4 times instead of just twice.
|
||||
class GridLayoutParams extends LayoutParams {
|
||||
/** The coordinates of this item in the grid. */
|
||||
/// The coordinates of this item in the grid. */
|
||||
int? row;
|
||||
int? column;
|
||||
int? rowSpan;
|
||||
int? columnSpan;
|
||||
@override
|
||||
int? layer;
|
||||
|
||||
/** Alignment within its box */
|
||||
/// Alignment within its box */
|
||||
GridItemAlignment rowAlign;
|
||||
GridItemAlignment columnAlign;
|
||||
|
||||
|
@ -89,16 +88,16 @@ class GridLayoutParams extends LayoutParams {
|
|||
columnSpan = rect.columnSpan;
|
||||
} else {
|
||||
// Apply default row, column span values.
|
||||
if (rowSpan == null) rowSpan = 1;
|
||||
if (columnSpan == null) columnSpan = 1;
|
||||
rowSpan ??= 1;
|
||||
columnSpan ??= 1;
|
||||
|
||||
if (row == null && column == null) {
|
||||
throw UnsupportedError('grid-flow is not implemented' +
|
||||
throw UnsupportedError('grid-flow is not implemented'
|
||||
' so at least one row or one column must be defined');
|
||||
}
|
||||
|
||||
if (row == null) row = 1;
|
||||
if (column == null) column = 1;
|
||||
row ??= 1;
|
||||
column ??= 1;
|
||||
}
|
||||
|
||||
assert(row! > 0 && rowSpan! > 0 && column! > 0 && columnSpan! > 0);
|
||||
|
|
|
@ -4,10 +4,8 @@
|
|||
|
||||
part of layout;
|
||||
|
||||
/**
|
||||
* Base class for simple recursive descent parsers.
|
||||
* Handles the lower level stuff, i.e. what a scanner/tokenizer would do.
|
||||
*/
|
||||
/// Base class for simple recursive descent parsers.
|
||||
/// Handles the lower level stuff, i.e. what a scanner/tokenizer would do.
|
||||
class _Parser {
|
||||
static const WHITESPACE = ' \r\n\t';
|
||||
|
||||
|
@ -124,7 +122,7 @@ class _Parser {
|
|||
return true;
|
||||
}
|
||||
|
||||
void _eat(String value, [bool eatWhitespace = true]) {
|
||||
void _eat(String value) {
|
||||
if (!_maybeEat(value)) {
|
||||
_error('expected "$value"');
|
||||
}
|
||||
|
@ -159,7 +157,7 @@ class _Parser {
|
|||
return result;
|
||||
}
|
||||
|
||||
/** Eats something like a keyword. */
|
||||
/// Eats something like a keyword. */
|
||||
String _eatWord() {
|
||||
int start = _offset;
|
||||
while (_offset < length && _isLetter(_peekChar())) {
|
||||
|
@ -168,7 +166,7 @@ class _Parser {
|
|||
return _src.substring(start, _offset);
|
||||
}
|
||||
|
||||
/** Eats an integer. */
|
||||
/// Eats an integer. */
|
||||
int? _maybeEatInt() {
|
||||
int start = _offset;
|
||||
bool dot = false;
|
||||
|
@ -183,7 +181,7 @@ class _Parser {
|
|||
return int.parse(_src.substring(start, _offset));
|
||||
}
|
||||
|
||||
/** Eats an integer. */
|
||||
/// Eats an integer. */
|
||||
int? _eatInt() {
|
||||
int? result = _maybeEatInt();
|
||||
if (result == null) {
|
||||
|
@ -192,7 +190,7 @@ class _Parser {
|
|||
return result;
|
||||
}
|
||||
|
||||
/** Eats something like a positive decimal: 12.345. */
|
||||
/// Eats something like a positive decimal: 12.345. */
|
||||
num _eatDouble() {
|
||||
int start = _offset;
|
||||
bool dot = false;
|
||||
|
@ -217,11 +215,11 @@ class _Parser {
|
|||
}
|
||||
}
|
||||
|
||||
/** Parses a grid template. */
|
||||
/// Parses a grid template. */
|
||||
class _GridTemplateParser extends _Parser {
|
||||
_GridTemplateParser._internal(String src) : super(src);
|
||||
|
||||
/** Parses the grid-rows and grid-columns CSS properties into object form. */
|
||||
/// Parses the grid-rows and grid-columns CSS properties into object form. */
|
||||
static GridTemplate? parse(String? str) {
|
||||
if (str == null) return null;
|
||||
final p = _GridTemplateParser._internal(str);
|
||||
|
@ -230,7 +228,7 @@ class _GridTemplateParser extends _Parser {
|
|||
return result;
|
||||
}
|
||||
|
||||
/** Parses a grid-cell value. */
|
||||
/// Parses a grid-cell value. */
|
||||
static String? parseCell(String? str) {
|
||||
if (str == null) return null;
|
||||
final p = _GridTemplateParser._internal(str);
|
||||
|
@ -249,18 +247,18 @@ class _GridTemplateParser extends _Parser {
|
|||
while ((row = _maybeEatString()) != null) {
|
||||
rows.add(row);
|
||||
}
|
||||
if (rows.length == 0) {
|
||||
if (rows.isEmpty) {
|
||||
_error('expected at least one cell, or "none"');
|
||||
}
|
||||
return GridTemplate(rows);
|
||||
}
|
||||
}
|
||||
|
||||
/** Parses a grid-row or grid-column */
|
||||
/// Parses a grid-row or grid-column */
|
||||
class _GridItemParser extends _Parser {
|
||||
_GridItemParser._internal(String src) : super(src);
|
||||
|
||||
/** Parses the grid-rows and grid-columns CSS properties into object form. */
|
||||
/// Parses the grid-rows and grid-columns CSS properties into object form. */
|
||||
static _GridLocation? parse(String? cell, GridTrackList? list) {
|
||||
if (cell == null) return null;
|
||||
final p = _GridItemParser._internal(cell);
|
||||
|
@ -281,7 +279,7 @@ class _GridItemParser extends _Parser {
|
|||
_error('expected row/column number or name');
|
||||
}
|
||||
int? end = _maybeParseLine(list);
|
||||
int? span = null;
|
||||
int? span;
|
||||
if (end != null) {
|
||||
span = end - start!;
|
||||
if (span <= 0) {
|
||||
|
@ -318,11 +316,9 @@ class _GridItemParser extends _Parser {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses grid-rows and grid-column properties, see:
|
||||
* [http://dev.w3.org/csswg/css3-grid-align/#grid-columns-and-rows-properties]
|
||||
* This is kept as a recursive descent parser for simplicity.
|
||||
*/
|
||||
/// Parses grid-rows and grid-column properties, see:
|
||||
/// [http://dev.w3.org/csswg/css3-grid-align/#grid-columns-and-rows-properties]
|
||||
/// This is kept as a recursive descent parser for simplicity.
|
||||
// TODO(jmesserly): implement missing features from the spec. Mainly around
|
||||
// CSS units, support for all escape sequences, etc.
|
||||
class _GridTrackParser extends _Parser {
|
||||
|
@ -331,10 +327,10 @@ class _GridTrackParser extends _Parser {
|
|||
|
||||
_GridTrackParser._internal(String src)
|
||||
: _tracks = <GridTrack>[],
|
||||
_lineNames = Map<String?, int>(),
|
||||
_lineNames = <String?, int>{},
|
||||
super(src);
|
||||
|
||||
/** Parses the grid-rows and grid-columns CSS properties into object form. */
|
||||
/// Parses the grid-rows and grid-columns CSS properties into object form. */
|
||||
static GridTrackList? parse(String? str) {
|
||||
if (str == null) return null;
|
||||
final p = _GridTrackParser._internal(str);
|
||||
|
@ -343,12 +339,10 @@ class _GridTrackParser extends _Parser {
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the grid-row-sizing and grid-column-sizing CSS properties into
|
||||
* object form.
|
||||
*/
|
||||
/// Parses the grid-row-sizing and grid-column-sizing CSS properties into
|
||||
/// object form.
|
||||
static TrackSizing parseTrackSizing(String? str) {
|
||||
if (str == null) str = 'auto';
|
||||
str ??= 'auto';
|
||||
final p = _GridTrackParser._internal(str);
|
||||
final result = p._parseTrackMinmax();
|
||||
p._eatEnd();
|
||||
|
@ -364,8 +358,8 @@ class _GridTrackParser extends _Parser {
|
|||
return GridTrackList(_tracks, _lineNames);
|
||||
}
|
||||
|
||||
/** Code shared by _parseTrackList and _parseTrackGroup */
|
||||
void _parseTrackListHelper([List<GridTrack>? resultTracks = null]) {
|
||||
/// Code shared by _parseTrackList and _parseTrackGroup */
|
||||
void _parseTrackListHelper([List<GridTrack>? resultTracks]) {
|
||||
_maybeEatWhitespace();
|
||||
while (!endOfInput) {
|
||||
String? name;
|
||||
|
@ -465,9 +459,7 @@ class _GridTrackParser extends _Parser {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception thrown because the grid style properties had incorrect values.
|
||||
*/
|
||||
/// Exception thrown because the grid style properties had incorrect values.
|
||||
class SyntaxErrorException implements Exception {
|
||||
final String _message;
|
||||
final int _offset;
|
||||
|
@ -475,6 +467,7 @@ class SyntaxErrorException implements Exception {
|
|||
|
||||
const SyntaxErrorException(this._message, this._source, this._offset);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
String location;
|
||||
if (_offset < _source.length) {
|
||||
|
|
|
@ -6,33 +6,27 @@ part of layout;
|
|||
|
||||
// This file has classes representing the grid tracks and grid template
|
||||
|
||||
/**
|
||||
* The data structure representing the grid-rows or grid-columns
|
||||
* properties.
|
||||
*/
|
||||
/// The data structure representing the grid-rows or grid-columns
|
||||
/// properties.
|
||||
class GridTrackList {
|
||||
/** The set of tracks defined in CSS via grid-rows and grid-columns */
|
||||
/// The set of tracks defined in CSS via grid-rows and grid-columns */
|
||||
final List<GridTrack> tracks;
|
||||
|
||||
/**
|
||||
* Maps edge names to the corresponding track. Depending on whether the index
|
||||
* is used as a start or end, it might be interpreted exclusively or
|
||||
* inclusively.
|
||||
*/
|
||||
/// Maps edge names to the corresponding track. Depending on whether the index
|
||||
/// is used as a start or end, it might be interpreted exclusively or
|
||||
/// inclusively.
|
||||
final Map<String?, int> lineNames;
|
||||
|
||||
GridTrackList(this.tracks, this.lineNames) {}
|
||||
GridTrackList(this.tracks, this.lineNames);
|
||||
}
|
||||
|
||||
/** Represents a row or a column. */
|
||||
/// Represents a row or a column. */
|
||||
class GridTrack {
|
||||
/**
|
||||
* The start position of this track. Equal to the sum of previous track's
|
||||
* usedBreadth.
|
||||
*/
|
||||
/// The start position of this track. Equal to the sum of previous track's
|
||||
/// usedBreadth.
|
||||
late num start;
|
||||
|
||||
/** The final computed breadth of this track. */
|
||||
/// The final computed breadth of this track. */
|
||||
late num usedBreadth;
|
||||
|
||||
// Fields used internally by the sizing algorithm
|
||||
|
@ -42,18 +36,16 @@ class GridTrack {
|
|||
|
||||
final TrackSizing sizing;
|
||||
|
||||
GridTrack(this.sizing) {}
|
||||
GridTrack(this.sizing);
|
||||
|
||||
/**
|
||||
* Support for the feature that repeats rows and columns, e.g.
|
||||
* [:grid-columns: 10px ("content" 250px 10px)[4]:]
|
||||
*/
|
||||
/// Support for the feature that repeats rows and columns, e.g.
|
||||
/// [:grid-columns: 10px ("content" 250px 10px)[4]:]
|
||||
GridTrack clone() => GridTrack(sizing.clone());
|
||||
|
||||
/** The min sizing function for the track. */
|
||||
/// The min sizing function for the track. */
|
||||
SizingFunction get minSizing => sizing.min;
|
||||
|
||||
/** The min sizing function for the track. */
|
||||
/// The min sizing function for the track. */
|
||||
SizingFunction get maxSizing => sizing.max;
|
||||
|
||||
num get end => start + usedBreadth;
|
||||
|
@ -61,14 +53,14 @@ class GridTrack {
|
|||
bool get isFractional => minSizing.isFraction || maxSizing.isFraction;
|
||||
}
|
||||
|
||||
/** Represents the grid-row-align or grid-column-align. */
|
||||
/// Represents the grid-row-align or grid-column-align. */
|
||||
class GridItemAlignment {
|
||||
// TODO(jmesserly): should this be stored as an int for performance?
|
||||
final String value;
|
||||
|
||||
// 'start' | 'end' | 'center' | 'stretch'
|
||||
GridItemAlignment.fromString(String? value)
|
||||
: this.value = (value == null) ? 'stretch' : value {
|
||||
: value = (value == null) ? 'stretch' : value {
|
||||
switch (this.value) {
|
||||
case 'start':
|
||||
case 'end':
|
||||
|
@ -98,10 +90,8 @@ class GridItemAlignment {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a grid-template. Used in conjunction with a grid-cell to
|
||||
* place cells in the grid, without needing to specify the exact row/column.
|
||||
*/
|
||||
/// Represents a grid-template. Used in conjunction with a grid-cell to
|
||||
/// place cells in the grid, without needing to specify the exact row/column.
|
||||
class GridTemplate {
|
||||
final Map<int, _GridTemplateRect> _rects;
|
||||
final int _numRows;
|
||||
|
@ -112,7 +102,7 @@ class GridTemplate {
|
|||
_buildRects(rows);
|
||||
}
|
||||
|
||||
/** Scans the template strings and computes bounds for each one. */
|
||||
/// Scans the template strings and computes bounds for each one. */
|
||||
void _buildRects(List<String?> templateRows) {
|
||||
for (int r = 0; r < templateRows.length; r++) {
|
||||
String row = templateRows[r]!;
|
||||
|
@ -133,9 +123,7 @@ class GridTemplate {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up the given cell in the template, and returns the rect.
|
||||
*/
|
||||
/// Looks up the given cell in the template, and returns the rect.
|
||||
_GridTemplateRect lookupCell(String cell) {
|
||||
if (cell.length != 1) {
|
||||
throw UnsupportedError(
|
||||
|
@ -150,13 +138,13 @@ class GridTemplate {
|
|||
}
|
||||
}
|
||||
|
||||
/** Used by GridTemplate to track a single cell's bounds. */
|
||||
/// Used by GridTemplate to track a single cell's bounds. */
|
||||
class _GridTemplateRect {
|
||||
int row, column, rowSpan, columnSpan, _count, _char;
|
||||
_GridTemplateRect(this._char, this.row, this.column)
|
||||
: rowSpan = 1,
|
||||
columnSpan = 1,
|
||||
_count = 1 {}
|
||||
_count = 1;
|
||||
|
||||
void add(int r, int c) {
|
||||
assert(r >= row && c >= column);
|
||||
|
@ -177,13 +165,11 @@ class _GridTemplateRect {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to return a row/column and span during parsing of grid-row and
|
||||
* grid-column during parsing.
|
||||
*/
|
||||
/// Used to return a row/column and span during parsing of grid-row and
|
||||
/// grid-column during parsing.
|
||||
class _GridLocation {
|
||||
final int? start, length;
|
||||
_GridLocation(this.start, this.length) {}
|
||||
_GridLocation(this.start, this.length);
|
||||
|
||||
int get end => start! + length!;
|
||||
}
|
||||
|
|
|
@ -6,9 +6,7 @@ part of layout;
|
|||
|
||||
// This file has classes representing the grid sizing functions
|
||||
|
||||
/**
|
||||
* Represents the sizing function used for the min or max of a row or column.
|
||||
*/
|
||||
/// Represents the sizing function used for the min or max of a row or column.
|
||||
// TODO(jmesserly): rename to GridSizing, or make internal
|
||||
class SizingFunction {
|
||||
const SizingFunction();
|
||||
|
@ -26,13 +24,11 @@ class SizingFunction {
|
|||
SizingFunction clone() => this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fixed size represents a length as defined by CSS3 Values spec.
|
||||
* Can also be a percentage of the Grid element's logical width (for columns)
|
||||
* or logical height (for rows). When the width or height of the Grid element
|
||||
* is undefined, the percentage is ignored and the Grid Track will be
|
||||
* auto-sized.
|
||||
*/
|
||||
/// Fixed size represents a length as defined by CSS3 Values spec.
|
||||
/// Can also be a percentage of the Grid element's logical width (for columns)
|
||||
/// or logical height (for rows). When the width or height of the Grid element
|
||||
/// is undefined, the percentage is ignored and the Grid Track will be
|
||||
/// auto-sized.
|
||||
class FixedSizing extends SizingFunction {
|
||||
final String units;
|
||||
final num length;
|
||||
|
@ -49,10 +45,13 @@ class FixedSizing extends SizingFunction {
|
|||
}
|
||||
|
||||
// TODO(jmesserly): this is only needed because of our mutable property
|
||||
@override
|
||||
FixedSizing clone() => FixedSizing(length, units);
|
||||
|
||||
@override
|
||||
bool get isMinContentSized => _contentSized;
|
||||
|
||||
@override
|
||||
num resolveLength(num? gridSize) {
|
||||
if (units == '%') {
|
||||
if (gridSize == null) {
|
||||
|
@ -68,60 +67,66 @@ class FixedSizing extends SizingFunction {
|
|||
}
|
||||
}
|
||||
|
||||
String toString() => 'FixedSizing: ${length}${units} $_contentSized';
|
||||
@override
|
||||
String toString() => 'FixedSizing: $length$units $_contentSized';
|
||||
}
|
||||
|
||||
/**
|
||||
* Fraction is a non-negative floating-point number followed by 'fr'. Each
|
||||
* fraction value takes a share of the remaining space proportional to its
|
||||
* number.
|
||||
*/
|
||||
/// Fraction is a non-negative floating-point number followed by 'fr'. Each
|
||||
/// fraction value takes a share of the remaining space proportional to its
|
||||
/// number.
|
||||
class FractionSizing extends SizingFunction {
|
||||
@override
|
||||
final num fractionValue;
|
||||
FractionSizing(this.fractionValue);
|
||||
|
||||
@override
|
||||
bool get isFraction => true;
|
||||
|
||||
@override
|
||||
String toString() => 'FixedSizing: ${fractionValue}fr';
|
||||
}
|
||||
|
||||
class MinContentSizing extends SizingFunction {
|
||||
const MinContentSizing();
|
||||
|
||||
@override
|
||||
bool get isMinContentSized => true;
|
||||
|
||||
@override
|
||||
String toString() => 'MinContentSizing';
|
||||
}
|
||||
|
||||
class MaxContentSizing extends SizingFunction {
|
||||
const MaxContentSizing();
|
||||
|
||||
@override
|
||||
bool get isMaxContentSized {
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'MaxContentSizing';
|
||||
}
|
||||
|
||||
/** The min and max sizing functions for a track. */
|
||||
/// The min and max sizing functions for a track. */
|
||||
class TrackSizing {
|
||||
/** The min sizing function for the track. */
|
||||
/// The min sizing function for the track. */
|
||||
final SizingFunction min;
|
||||
|
||||
/** The min sizing function for the track. */
|
||||
/// The min sizing function for the track. */
|
||||
final SizingFunction max;
|
||||
|
||||
const TrackSizing.auto()
|
||||
: min = const MinContentSizing(),
|
||||
max = const MaxContentSizing();
|
||||
|
||||
TrackSizing(this.min, this.max) {}
|
||||
TrackSizing(this.min, this.max);
|
||||
|
||||
// TODO(jmesserly): this is only needed because FixedSizing is mutable
|
||||
TrackSizing clone() => TrackSizing(min.clone(), max.clone());
|
||||
}
|
||||
|
||||
/** Represents a GridTrack breadth property. */
|
||||
/// Represents a GridTrack breadth property. */
|
||||
// TODO(jmesserly): these classes could be replaced with reflection/mirrors
|
||||
abstract class _BreadthAccumulator {
|
||||
void setSize(GridTrack? t, num value);
|
||||
|
@ -133,23 +138,29 @@ abstract class _BreadthAccumulator {
|
|||
class _UsedBreadthAccumulator implements _BreadthAccumulator {
|
||||
const _UsedBreadthAccumulator();
|
||||
|
||||
@override
|
||||
void setSize(GridTrack? t, num value) {
|
||||
t!.usedBreadth = value;
|
||||
}
|
||||
|
||||
@override
|
||||
num? getSize(GridTrack? t) => t!.usedBreadth;
|
||||
|
||||
@override
|
||||
SizingFunction getSizingFunction(GridTrack? t) => t!.minSizing;
|
||||
}
|
||||
|
||||
class _MaxBreadthAccumulator implements _BreadthAccumulator {
|
||||
const _MaxBreadthAccumulator();
|
||||
|
||||
@override
|
||||
void setSize(GridTrack? t, num value) {
|
||||
t!.maxBreadth = value;
|
||||
}
|
||||
|
||||
@override
|
||||
num? getSize(GridTrack? t) => t!.maxBreadth;
|
||||
|
||||
@override
|
||||
SizingFunction getSizingFunction(GridTrack? t) => t!.maxSizing;
|
||||
}
|
||||
|
|
|
@ -4,27 +4,25 @@
|
|||
|
||||
part of layout;
|
||||
|
||||
/** The interface that the layout algorithms use to talk to the view. */
|
||||
/// The interface that the layout algorithms use to talk to the view. */
|
||||
abstract class Positionable {
|
||||
ViewLayout get layout;
|
||||
|
||||
/** Gets our custom CSS properties, as provided by the CSS preprocessor. */
|
||||
/// Gets our custom CSS properties, as provided by the CSS preprocessor. */
|
||||
Map<String, String> get customStyle;
|
||||
|
||||
/** Gets the root DOM used for layout. */
|
||||
/// Gets the root DOM used for layout. */
|
||||
Element get node;
|
||||
|
||||
/** Gets the collection of child views. */
|
||||
/// Gets the collection of child views. */
|
||||
Iterable<Positionable> get childViews;
|
||||
|
||||
/** Causes a view to layout its children. */
|
||||
/// Causes a view to layout its children. */
|
||||
void doLayout();
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches the layout parameters that were specified in CSS during a layout
|
||||
* computation. These values are immutable during a layout.
|
||||
*/
|
||||
/// Caches the layout parameters that were specified in CSS during a layout
|
||||
/// computation. These values are immutable during a layout.
|
||||
class LayoutParams {
|
||||
// TODO(jmesserly): should be const, but there's a bug in DartC preventing us
|
||||
// from calling "window." in an initializer. See b/5332777
|
||||
|
@ -40,19 +38,19 @@ class LayoutParams {
|
|||
// TODO(jmesserly): enums would really help here
|
||||
class Dimension {
|
||||
// TODO(jmesserly): perhaps this should be X and Y
|
||||
static const WIDTH = const Dimension._internal('width');
|
||||
static const HEIGHT = const Dimension._internal('height');
|
||||
static const WIDTH = Dimension._internal('width');
|
||||
static const HEIGHT = Dimension._internal('height');
|
||||
|
||||
final String name; // for debugging
|
||||
const Dimension._internal(this.name);
|
||||
}
|
||||
|
||||
class ContentSizeMode {
|
||||
/** Minimum content size, e.g. min-width and min-height in CSS. */
|
||||
static const MIN = const ContentSizeMode._internal('min');
|
||||
/// Minimum content size, e.g. min-width and min-height in CSS. */
|
||||
static const MIN = ContentSizeMode._internal('min');
|
||||
|
||||
/** Maximum content size, e.g. min-width and min-height in CSS. */
|
||||
static const MAX = const ContentSizeMode._internal('max');
|
||||
/// Maximum content size, e.g. min-width and min-height in CSS. */
|
||||
static const MAX = ContentSizeMode._internal('max');
|
||||
|
||||
// TODO(jmesserly): we probably want some sort of "auto" or "best fit" mode
|
||||
// Don't need it yet though.
|
||||
|
@ -61,35 +59,27 @@ class ContentSizeMode {
|
|||
const ContentSizeMode._internal(this.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base class for View layout. Tracks relevant layout state.
|
||||
* This code was inspired by code in Android's View.java; it's needed for the
|
||||
* rest of the layout system.
|
||||
*/
|
||||
/// Abstract base class for View layout. Tracks relevant layout state.
|
||||
/// This code was inspired by code in Android's View.java; it's needed for the
|
||||
/// rest of the layout system.
|
||||
class ViewLayout {
|
||||
/**
|
||||
* The layout parameters associated with this view and used by the parent
|
||||
* to determine how this view should be laid out.
|
||||
*/
|
||||
/// The layout parameters associated with this view and used by the parent
|
||||
/// to determine how this view should be laid out.
|
||||
LayoutParams? layoutParams;
|
||||
int? _offsetWidth;
|
||||
int? _offsetHeight;
|
||||
|
||||
/** The view that this layout belongs to. */
|
||||
/// The view that this layout belongs to. */
|
||||
final Positionable view;
|
||||
|
||||
/**
|
||||
* To get a perforant positioning model on top of the DOM, we read all
|
||||
* properties in the first pass while computing positions. Then we have a
|
||||
* second pass that actually moves everything.
|
||||
*/
|
||||
/// To get a perforant positioning model on top of the DOM, we read all
|
||||
/// properties in the first pass while computing positions. Then we have a
|
||||
/// second pass that actually moves everything.
|
||||
int? _measuredLeft, _measuredTop, _measuredWidth, _measuredHeight;
|
||||
|
||||
ViewLayout(this.view);
|
||||
|
||||
/**
|
||||
* Creates the appropriate view layout, depending on the properties.
|
||||
*/
|
||||
/// Creates the appropriate view layout, depending on the properties.
|
||||
// TODO(jmesserly): we should support user defined layouts somehow. Perhaps
|
||||
// registered with a LayoutProvider.
|
||||
factory ViewLayout.fromView(Positionable view) {
|
||||
|
@ -126,13 +116,11 @@ class ViewLayout {
|
|||
int get borderWidth => borderLeftWidth + borderRightWidth;
|
||||
int get borderHeight => borderTopWidth + borderBottomWidth;
|
||||
|
||||
/** Implements the custom layout computation. */
|
||||
/// Implements the custom layout computation. */
|
||||
void measureLayout(Future<Size> size, Completer<bool>? changed) {}
|
||||
|
||||
/**
|
||||
* Positions the view within its parent container.
|
||||
* Also performs a layout of its children.
|
||||
*/
|
||||
/// Positions the view within its parent container.
|
||||
/// Also performs a layout of its children.
|
||||
void setBounds(int? left, int? top, int width, int height) {
|
||||
assert(width >= 0 && height >= 0);
|
||||
|
||||
|
@ -147,7 +135,7 @@ class ViewLayout {
|
|||
measureLayout(completer.future, null);
|
||||
}
|
||||
|
||||
/** Applies the layout to the node. */
|
||||
/// Applies the layout to the node. */
|
||||
void applyLayout() {
|
||||
if (_measuredLeft != null) {
|
||||
// TODO(jmesserly): benchmark the performance of this DOM interaction
|
||||
|
@ -180,12 +168,13 @@ class ViewLayout {
|
|||
}
|
||||
|
||||
int? measureContent(ViewLayout parent, Dimension? dimension,
|
||||
[ContentSizeMode? mode = null]) {
|
||||
[ContentSizeMode? mode]) {
|
||||
if (dimension == Dimension.WIDTH) {
|
||||
return measureWidth(parent, mode);
|
||||
} else if (dimension == Dimension.HEIGHT) {
|
||||
return measureHeight(parent, mode);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
int? measureWidth(ViewLayout parent, ContentSizeMode? mode) {
|
||||
|
@ -195,6 +184,7 @@ class ViewLayout {
|
|||
} else if (mode == ContentSizeMode.MAX) {
|
||||
return _styleToPixels(style!.maxWidth, currentWidth, parent.currentWidth);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
int? measureHeight(ViewLayout parent, ContentSizeMode? mode) {
|
||||
|
@ -206,6 +196,7 @@ class ViewLayout {
|
|||
return _styleToPixels(
|
||||
style!.maxHeight, currentHeight, parent.currentHeight);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static int _toPixels(String style) {
|
||||
|
|
|
@ -4,58 +4,54 @@
|
|||
|
||||
part of observable;
|
||||
|
||||
/** A change to an observable instance. */
|
||||
/// A change to an observable instance. */
|
||||
class ChangeEvent {
|
||||
// TODO(sigmund): capture language issues around enums & create a canonical
|
||||
// Dart enum design.
|
||||
/** Type denoting an in-place update event. */
|
||||
/// Type denoting an in-place update event. */
|
||||
static const UPDATE = 0;
|
||||
|
||||
/** Type denoting an insertion event. */
|
||||
/// Type denoting an insertion event. */
|
||||
static const INSERT = 1;
|
||||
|
||||
/** Type denoting a single-remove event. */
|
||||
/// Type denoting a single-remove event. */
|
||||
static const REMOVE = 2;
|
||||
|
||||
/**
|
||||
* Type denoting events that affect the entire observable instance. For
|
||||
* example, a list operation like clear or sort.
|
||||
*/
|
||||
/// Type denoting events that affect the entire observable instance. For
|
||||
/// example, a list operation like clear or sort.
|
||||
static const GLOBAL = 3;
|
||||
|
||||
/** The observable instance that changed. */
|
||||
/// The observable instance that changed. */
|
||||
final Observable target;
|
||||
|
||||
/** Whether the change was an [INSERT], [REMOVE], or [UPDATE]. */
|
||||
/// Whether the change was an [INSERT], [REMOVE], or [UPDATE]. */
|
||||
final int type;
|
||||
|
||||
/** The value after the change (or inserted value in a list). */
|
||||
/// The value after the change (or inserted value in a list). */
|
||||
final newValue;
|
||||
|
||||
/** The value before the change (or removed value from a list). */
|
||||
/// The value before the change (or removed value from a list). */
|
||||
final oldValue;
|
||||
|
||||
/** Property that changed (null for list changes). */
|
||||
/// Property that changed (null for list changes). */
|
||||
final String? propertyName;
|
||||
|
||||
/**
|
||||
* Index of the list operation. Insertions prepend in front of the given
|
||||
* index (insert at 0 means an insertion at the beginning of the list).
|
||||
*/
|
||||
/// Index of the list operation. Insertions prepend in front of the given
|
||||
/// index (insert at 0 means an insertion at the beginning of the list).
|
||||
final int? index;
|
||||
|
||||
/** Factory constructor for property change events. */
|
||||
/// Factory constructor for property change events. */
|
||||
ChangeEvent.property(
|
||||
this.target, this.propertyName, this.newValue, this.oldValue)
|
||||
: type = UPDATE,
|
||||
index = null;
|
||||
|
||||
/** Factory constructor for list change events. */
|
||||
/// Factory constructor for list change events. */
|
||||
ChangeEvent.list(
|
||||
this.target, this.type, this.index, this.newValue, this.oldValue)
|
||||
: propertyName = null;
|
||||
|
||||
/** Factory constructor for [GLOBAL] change events. */
|
||||
/// Factory constructor for [GLOBAL] change events. */
|
||||
ChangeEvent.global(this.target)
|
||||
: type = GLOBAL,
|
||||
newValue = null,
|
||||
|
@ -64,7 +60,7 @@ class ChangeEvent {
|
|||
index = null;
|
||||
}
|
||||
|
||||
/** A collection of change events on a single observable instance. */
|
||||
/// A collection of change events on a single observable instance. */
|
||||
class EventSummary {
|
||||
final Observable target;
|
||||
|
||||
|
@ -77,9 +73,9 @@ class EventSummary {
|
|||
events.add(e);
|
||||
}
|
||||
|
||||
/** Notify listeners of [target] and parents of [target] about all changes. */
|
||||
/// Notify listeners of [target] and parents of [target] about all changes. */
|
||||
void notify() {
|
||||
if (!events.isEmpty) {
|
||||
if (events.isNotEmpty) {
|
||||
for (Observable? obj = target; obj != null; obj = obj.parent) {
|
||||
for (final listener in obj.listeners) {
|
||||
listener(this);
|
||||
|
@ -89,5 +85,5 @@ class EventSummary {
|
|||
}
|
||||
}
|
||||
|
||||
/** A listener of change events. */
|
||||
typedef void ChangeListener(EventSummary events);
|
||||
/// A listener of change events. */
|
||||
typedef ChangeListener = void Function(EventSummary events);
|
||||
|
|
|
@ -4,43 +4,37 @@
|
|||
|
||||
part of observable;
|
||||
|
||||
/**
|
||||
* Accumulates change events from several observable objects.
|
||||
*
|
||||
* wrap() is public and used by client code. The other methods are used by
|
||||
* AbstractObservable, which works with this class to implement batching.
|
||||
*/
|
||||
/// Accumulates change events from several observable objects.
|
||||
///
|
||||
/// wrap() is public and used by client code. The other methods are used by
|
||||
/// AbstractObservable, which works with this class to implement batching.
|
||||
class EventBatch {
|
||||
/** The current active batch, if any. */
|
||||
/// The current active batch, if any. */
|
||||
static EventBatch? current;
|
||||
|
||||
/** Used to generate unique ids for observable objects. */
|
||||
/// Used to generate unique ids for observable objects. */
|
||||
static int nextUid = 1;
|
||||
|
||||
/** Map from observable object's uid to their tracked events. */
|
||||
/// Map from observable object's uid to their tracked events. */
|
||||
// TODO(sigmund): use [Observable] instead of [int] when [Map] can support it,
|
||||
Map<int, EventSummary> summaries;
|
||||
|
||||
/** Whether this batch is currently firing and therefore is sealed. */
|
||||
/// Whether this batch is currently firing and therefore is sealed. */
|
||||
bool sealed = false;
|
||||
|
||||
/**
|
||||
* Private constructor that shouldn't be used externally. Use [wrap] to ensure
|
||||
* that a batch exists when running a function.
|
||||
*/
|
||||
EventBatch._internal() : summaries = Map<int, EventSummary>();
|
||||
/// Private constructor that shouldn't be used externally. Use [wrap] to ensure
|
||||
/// that a batch exists when running a function.
|
||||
EventBatch._internal() : summaries = <int, EventSummary>{};
|
||||
|
||||
/**
|
||||
* Ensure there is an event batch where [userFunction] can accumulate events.
|
||||
* When the batch is complete, fire all events at once.
|
||||
*/
|
||||
/// Ensure there is an event batch where [userFunction] can accumulate events.
|
||||
/// When the batch is complete, fire all events at once.
|
||||
static Function wrap(userFunction(var a)) {
|
||||
return (e) {
|
||||
if (current == null) {
|
||||
// Not in a batch so create one.
|
||||
final batch = EventBatch._internal();
|
||||
current = batch;
|
||||
var result = null;
|
||||
var result;
|
||||
try {
|
||||
// TODO(jmesserly): don't return here, otherwise an exception in
|
||||
// the finally clause will cause it to rerun. See bug#5350131.
|
||||
|
@ -74,12 +68,12 @@ class EventBatch {
|
|||
};
|
||||
}
|
||||
|
||||
/** Returns a unique global id for observable objects. */
|
||||
/// Returns a unique global id for observable objects. */
|
||||
static int genUid() {
|
||||
return nextUid++;
|
||||
}
|
||||
|
||||
/** Retrieves the events associated with {@code obj}. */
|
||||
/// Retrieves the events associated with {@code obj}. */
|
||||
EventSummary getEvents(Observable obj) {
|
||||
int uid = obj.uid;
|
||||
EventSummary? summary = summaries[uid];
|
||||
|
@ -91,7 +85,7 @@ class EventBatch {
|
|||
return summary;
|
||||
}
|
||||
|
||||
/** Fires all events at once. */
|
||||
/// Fires all events at once. */
|
||||
void _notify() {
|
||||
assert(!sealed);
|
||||
sealed = true;
|
||||
|
|
|
@ -7,61 +7,59 @@ library observable;
|
|||
part 'ChangeEvent.dart';
|
||||
part 'EventBatch.dart';
|
||||
|
||||
/**
|
||||
* An object whose changes are tracked and who can issue events notifying how it
|
||||
* has been changed.
|
||||
*/
|
||||
/// An object whose changes are tracked and who can issue events notifying how it
|
||||
/// has been changed.
|
||||
abstract class Observable {
|
||||
/** Returns a globally unique identifier for the object. */
|
||||
/// Returns a globally unique identifier for the object. */
|
||||
// TODO(sigmund): remove once dart supports maps with arbitrary keys.
|
||||
int get uid;
|
||||
|
||||
/** Listeners on this model. */
|
||||
/// Listeners on this model. */
|
||||
List<ChangeListener> get listeners;
|
||||
|
||||
/** The parent observable to notify when this child is changed. */
|
||||
/// The parent observable to notify when this child is changed. */
|
||||
Observable? get parent;
|
||||
|
||||
/**
|
||||
* Adds a listener for changes on this observable instance. Returns whether
|
||||
* the listener was added successfully.
|
||||
*/
|
||||
/// Adds a listener for changes on this observable instance. Returns whether
|
||||
/// the listener was added successfully.
|
||||
bool addChangeListener(ChangeListener listener);
|
||||
|
||||
/**
|
||||
* Removes a listener for changes on this observable instance. Returns whether
|
||||
* the listener was removed successfully.
|
||||
*/
|
||||
/// Removes a listener for changes on this observable instance. Returns whether
|
||||
/// the listener was removed successfully.
|
||||
bool removeChangeListener(ChangeListener listener);
|
||||
}
|
||||
|
||||
/** Common functionality for observable objects. */
|
||||
/// Common functionality for observable objects. */
|
||||
class AbstractObservable implements Observable {
|
||||
/** Unique id to identify this model in an event batch. */
|
||||
/// Unique id to identify this model in an event batch. */
|
||||
@override
|
||||
final int uid;
|
||||
|
||||
/** The parent observable to notify when this child is changed. */
|
||||
/// The parent observable to notify when this child is changed. */
|
||||
@override
|
||||
final Observable? parent;
|
||||
|
||||
/** Listeners on this model. */
|
||||
/// Listeners on this model. */
|
||||
@override
|
||||
List<ChangeListener> listeners;
|
||||
|
||||
/** Whether this object is currently observed by listeners or propagators. */
|
||||
/// Whether this object is currently observed by listeners or propagators. */
|
||||
bool get isObserved {
|
||||
for (Observable? obj = this; obj != null; obj = obj.parent) {
|
||||
if (listeners.length > 0) {
|
||||
if (listeners.isNotEmpty) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
AbstractObservable([this.parent = null])
|
||||
AbstractObservable([this.parent])
|
||||
: uid = EventBatch.genUid(),
|
||||
listeners = List<ChangeListener>.empty();
|
||||
|
||||
@override
|
||||
bool addChangeListener(ChangeListener listener) {
|
||||
if (listeners.indexOf(listener, 0) == -1) {
|
||||
if (!listeners.contains(listener)) {
|
||||
listeners.add(listener);
|
||||
return true;
|
||||
}
|
||||
|
@ -69,6 +67,7 @@ class AbstractObservable implements Observable {
|
|||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
bool removeChangeListener(ChangeListener listener) {
|
||||
var index = listeners.indexOf(listener, 0);
|
||||
if (index != -1) {
|
||||
|
@ -126,62 +125,77 @@ class AbstractObservable implements Observable {
|
|||
}
|
||||
}
|
||||
|
||||
/** A growable list that fires events when it's modified. */
|
||||
/// A growable list that fires events when it's modified. */
|
||||
class ObservableList<T> extends AbstractObservable
|
||||
implements List<T>, Observable {
|
||||
/** Underlying list. */
|
||||
/// Underlying list. */
|
||||
// TODO(rnystrom): Make this final if we get list.remove().
|
||||
List<T> _internal;
|
||||
final List<T> _internal;
|
||||
|
||||
ObservableList([Observable? parent = null])
|
||||
ObservableList([Observable? parent])
|
||||
: _internal = List<T>.empty(),
|
||||
super(parent);
|
||||
|
||||
@override
|
||||
T operator [](int index) => _internal[index];
|
||||
|
||||
@override
|
||||
void operator []=(int index, T value) {
|
||||
recordListUpdate(index, value, _internal[index]);
|
||||
_internal[index] = value;
|
||||
}
|
||||
|
||||
@override
|
||||
int get length => _internal.length;
|
||||
|
||||
@override
|
||||
List<R> cast<R>() => _internal.cast<R>();
|
||||
@override
|
||||
Iterable<R> whereType<R>() => _internal.whereType<R>();
|
||||
|
||||
@override
|
||||
List<T> operator +(List<T> other) => _internal + other;
|
||||
|
||||
@override
|
||||
Iterable<T> followedBy(Iterable<T> other) => _internal.followedBy(other);
|
||||
|
||||
int indexWhere(bool test(T element), [int start = 0]) =>
|
||||
@override
|
||||
int indexWhere(bool Function(T element) test, [int start = 0]) =>
|
||||
_internal.indexWhere(test, start);
|
||||
|
||||
int lastIndexWhere(bool test(T element), [int? start]) =>
|
||||
@override
|
||||
int lastIndexWhere(bool Function(T element) test, [int? start]) =>
|
||||
_internal.lastIndexWhere(test, start);
|
||||
|
||||
void set length(int value) {
|
||||
@override
|
||||
set length(int value) {
|
||||
_internal.length = value;
|
||||
recordGlobalChange();
|
||||
}
|
||||
|
||||
@override
|
||||
void clear() {
|
||||
_internal.clear();
|
||||
recordGlobalChange();
|
||||
}
|
||||
|
||||
@override
|
||||
Iterable<T> get reversed => _internal.reversed;
|
||||
|
||||
void sort([int compare(T a, T b)?]) {
|
||||
@override
|
||||
void sort([int Function(T a, T b)? compare]) {
|
||||
//if (compare == null) compare = (u, v) => Comparable.compare(u, v);
|
||||
_internal.sort(compare);
|
||||
recordGlobalChange();
|
||||
}
|
||||
|
||||
@override
|
||||
void add(T element) {
|
||||
recordListInsert(length, element);
|
||||
_internal.add(element);
|
||||
}
|
||||
|
||||
@override
|
||||
void addAll(Iterable<T> elements) {
|
||||
for (T element in elements) {
|
||||
add(element);
|
||||
|
@ -194,49 +208,61 @@ class ObservableList<T> extends AbstractObservable
|
|||
return _internal.length;
|
||||
}
|
||||
|
||||
@override
|
||||
T get first => _internal.first;
|
||||
void set first(T value) {
|
||||
@override
|
||||
set first(T value) {
|
||||
_internal.first = value;
|
||||
}
|
||||
|
||||
@override
|
||||
T get last => _internal.last;
|
||||
void set last(T value) {
|
||||
@override
|
||||
set last(T value) {
|
||||
_internal.last = value;
|
||||
}
|
||||
|
||||
@override
|
||||
T get single => _internal.single;
|
||||
|
||||
@override
|
||||
void insert(int index, T element) {
|
||||
_internal.insert(index, element);
|
||||
recordListInsert(index, element);
|
||||
}
|
||||
|
||||
@override
|
||||
void insertAll(int index, Iterable<T> iterable) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
void setAll(int index, Iterable<T> iterable) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
T removeLast() {
|
||||
final result = _internal.removeLast();
|
||||
recordListRemove(length, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
T removeAt(int index) {
|
||||
T result = _internal.removeAt(index);
|
||||
recordListRemove(index, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@override
|
||||
int indexOf(T element, [int start = 0]) {
|
||||
return _internal.indexOf(element, start);
|
||||
}
|
||||
|
||||
@override
|
||||
int lastIndexOf(T element, [int? start]) {
|
||||
if (start == null) start = length - 1;
|
||||
start ??= length - 1;
|
||||
return _internal.lastIndexOf(element, start);
|
||||
}
|
||||
|
||||
|
@ -261,8 +287,8 @@ class ObservableList<T> extends AbstractObservable
|
|||
|
||||
void copyFrom(List<T> src, int? srcStart, int? dstStart, int count) {
|
||||
List dst = this;
|
||||
if (srcStart == null) srcStart = 0;
|
||||
if (dstStart == null) dstStart = 0;
|
||||
srcStart ??= 0;
|
||||
dstStart ??= 0;
|
||||
|
||||
if (srcStart < dstStart) {
|
||||
for (int i = srcStart + count - 1, j = dstStart + count - 1;
|
||||
|
@ -277,88 +303,125 @@ class ObservableList<T> extends AbstractObservable
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void setRange(int start, int end, Iterable iterable, [int skipCount = 0]) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
void removeRange(int start, int end) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
void replaceRange(int start, int end, Iterable<T> iterable) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
void fillRange(int start, int end, [T? fillValue]) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
List<T> sublist(int start, [int? end]) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Iterable<T> getRange(int start, int end) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
bool contains(Object? element) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
T reduce(T combine(T previousValue, T element)) {
|
||||
@override
|
||||
T reduce(T Function(T previousValue, T element) combine) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
R fold<R>(R initialValue, R combine(R previousValue, T element)) {
|
||||
@override
|
||||
R fold<R>(R initialValue, R Function(R previousValue, T element) combine) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
// Iterable<T>:
|
||||
@override
|
||||
Iterator<T> get iterator => _internal.iterator;
|
||||
|
||||
Iterable<T> where(bool f(T element)) => _internal.where(f);
|
||||
Iterable<R> map<R>(R f(T element)) => _internal.map(f);
|
||||
Iterable<R> expand<R>(Iterable<R> f(T element)) => _internal.expand(f);
|
||||
@override
|
||||
Iterable<T> where(bool Function(T element) f) => _internal.where(f);
|
||||
@override
|
||||
Iterable<R> map<R>(R Function(T element) f) => _internal.map(f);
|
||||
@override
|
||||
Iterable<R> expand<R>(Iterable<R> Function(T element) f) =>
|
||||
_internal.expand(f);
|
||||
@override
|
||||
List<T> skip(int count) => _internal.skip(count) as List<T>;
|
||||
@override
|
||||
List<T> take(int count) => _internal.take(count) as List<T>;
|
||||
bool every(bool f(T element)) => _internal.every(f);
|
||||
bool any(bool f(T element)) => _internal.any(f);
|
||||
void forEach(void f(T element)) {
|
||||
@override
|
||||
bool every(bool Function(T element) f) => _internal.every(f);
|
||||
@override
|
||||
bool any(bool Function(T element) f) => _internal.any(f);
|
||||
@override
|
||||
void forEach(void Function(T element) f) {
|
||||
_internal.forEach(f);
|
||||
}
|
||||
|
||||
@override
|
||||
String join([String separator = ""]) => _internal.join(separator);
|
||||
T firstWhere(bool test(T value), {T orElse()?}) {
|
||||
@override
|
||||
T firstWhere(bool Function(T value) test, {T Function()? orElse}) {
|
||||
return _internal.firstWhere(test, orElse: orElse);
|
||||
}
|
||||
|
||||
T lastWhere(bool test(T value), {T orElse()?}) {
|
||||
@override
|
||||
T lastWhere(bool Function(T value) test, {T Function()? orElse}) {
|
||||
return _internal.lastWhere(test, orElse: orElse);
|
||||
}
|
||||
|
||||
@override
|
||||
void shuffle([random]) => throw UnimplementedError();
|
||||
@override
|
||||
bool remove(Object? element) => throw UnimplementedError();
|
||||
void removeWhere(bool test(T element)) => throw UnimplementedError();
|
||||
void retainWhere(bool test(T element)) => throw UnimplementedError();
|
||||
List<T> toList({bool growable: true}) => throw UnimplementedError();
|
||||
@override
|
||||
void removeWhere(bool Function(T element) test) => throw UnimplementedError();
|
||||
@override
|
||||
void retainWhere(bool Function(T element) test) => throw UnimplementedError();
|
||||
@override
|
||||
List<T> toList({bool growable = true}) => throw UnimplementedError();
|
||||
@override
|
||||
Set<T> toSet() => throw UnimplementedError();
|
||||
Iterable<T> takeWhile(bool test(T value)) => throw UnimplementedError();
|
||||
Iterable<T> skipWhile(bool test(T value)) => throw UnimplementedError();
|
||||
@override
|
||||
Iterable<T> takeWhile(bool Function(T value) test) =>
|
||||
throw UnimplementedError();
|
||||
@override
|
||||
Iterable<T> skipWhile(bool Function(T value) test) =>
|
||||
throw UnimplementedError();
|
||||
|
||||
T singleWhere(bool test(T value), {T orElse()?}) {
|
||||
@override
|
||||
T singleWhere(bool Function(T value) test, {T Function()? orElse}) {
|
||||
return _internal.singleWhere(test, orElse: orElse);
|
||||
}
|
||||
|
||||
@override
|
||||
T elementAt(int index) {
|
||||
return _internal.elementAt(index);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<int, T> asMap() {
|
||||
return _internal.asMap();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isEmpty => length == 0;
|
||||
|
||||
@override
|
||||
bool get isNotEmpty => !isEmpty;
|
||||
}
|
||||
|
||||
|
@ -368,15 +431,15 @@ class ObservableList<T> extends AbstractObservable
|
|||
// much. Also, making a value observable necessitates adding ".value" to lots
|
||||
// of places, and constructing all fields with the verbose
|
||||
// "ObservableValue<DataType>(myValue)".
|
||||
/** A wrapper around a single value whose change can be observed. */
|
||||
/// A wrapper around a single value whose change can be observed. */
|
||||
class ObservableValue<T> extends AbstractObservable {
|
||||
ObservableValue(T value, [Observable? parent = null])
|
||||
ObservableValue(T value, [Observable? parent])
|
||||
: _value = value,
|
||||
super(parent);
|
||||
|
||||
T get value => _value;
|
||||
|
||||
void set value(T newValue) {
|
||||
set value(T newValue) {
|
||||
// Only fire on an actual change.
|
||||
if (!identical(newValue, _value)) {
|
||||
final oldValue = _value;
|
||||
|
|
|
@ -6,40 +6,34 @@
|
|||
|
||||
part of touch;
|
||||
|
||||
/**
|
||||
* Functions to model constant acceleration as a cubic Bezier
|
||||
* curve (http://en.wikipedia.org/wiki/Bezier_curve). These functions are
|
||||
* intended to generate the transition timing function for CSS transitions.
|
||||
* Please see
|
||||
* [http://www.w3.org/TR/css3-transitions/#transition-timing-function_tag].
|
||||
*
|
||||
* The main operation of computing a cubic Bezier is split up into multiple
|
||||
* functions so that, should it be required, more operations and cases can be
|
||||
* supported in the future.
|
||||
*/
|
||||
/// Functions to model constant acceleration as a cubic Bezier
|
||||
/// curve (http://en.wikipedia.org/wiki/Bezier_curve). These functions are
|
||||
/// intended to generate the transition timing function for CSS transitions.
|
||||
/// Please see
|
||||
/// [http://www.w3.org/TR/css3-transitions/#transition-timing-function_tag].
|
||||
///
|
||||
/// The main operation of computing a cubic Bezier is split up into multiple
|
||||
/// functions so that, should it be required, more operations and cases can be
|
||||
/// supported in the future.
|
||||
class BezierPhysics {
|
||||
static const _ONE_THIRD = 1 / 3;
|
||||
static const _TWO_THIRDS = 2 / 3;
|
||||
|
||||
/**
|
||||
* A list [:[x1, y1, x2, y2]:] of the intermediate control points of a cubic
|
||||
* bezier when the final velocity is zero. This is a special case for which
|
||||
* these control points are constants.
|
||||
*/
|
||||
static const List<num> _FINAL_VELOCITY_ZERO_BEZIER = const [
|
||||
/// A list [:[x1, y1, x2, y2]:] of the intermediate control points of a cubic
|
||||
/// bezier when the final velocity is zero. This is a special case for which
|
||||
/// these control points are constants.
|
||||
static const List<num> _FINAL_VELOCITY_ZERO_BEZIER = [
|
||||
_ONE_THIRD,
|
||||
_TWO_THIRDS,
|
||||
_TWO_THIRDS,
|
||||
1
|
||||
];
|
||||
|
||||
/**
|
||||
* Given consistent kinematics parameters for constant acceleration, returns
|
||||
* the intermediate control points of the cubic Bezier curve that models the
|
||||
* motion. All input values must have correct signs.
|
||||
* Returns a list [:[x1, y1, x2, y2]:] representing the intermediate control
|
||||
* points of the cubic Bezier.
|
||||
*/
|
||||
/// Given consistent kinematics parameters for constant acceleration, returns
|
||||
/// the intermediate control points of the cubic Bezier curve that models the
|
||||
/// motion. All input values must have correct signs.
|
||||
/// Returns a list [:[x1, y1, x2, y2]:] representing the intermediate control
|
||||
/// points of the cubic Bezier.
|
||||
static List<num> calculateCubicBezierFromKinematics(num initialVelocity,
|
||||
num finalVelocity, num totalTime, num totalDisplacement) {
|
||||
// Total time must be greater than 0.
|
||||
|
@ -61,18 +55,16 @@ class BezierPhysics {
|
|||
return _quadraticToCubic(controlPoint[0], controlPoint[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a quadratic curve crossing points (0, 0) and (x2, y2), calculates the
|
||||
* intermediate control point (x1, y1) of the equivalent quadratic Bezier
|
||||
* curve with starting point (0, 0) and ending point (x2, y2).
|
||||
* [m0] The slope of the line tangent to the curve at (0, 0).
|
||||
* [m2] The slope of the line tangent to the curve at a different
|
||||
* point (x2, y2).
|
||||
* [x2] The x-coordinate of the other point on the curve.
|
||||
* [y2] The y-coordinate of the other point on the curve.
|
||||
* Returns a list [:[x1, y1]:] representing the intermediate
|
||||
* control point of the quadratic Bezier.
|
||||
*/
|
||||
/// Given a quadratic curve crossing points (0, 0) and (x2, y2), calculates the
|
||||
/// intermediate control point (x1, y1) of the equivalent quadratic Bezier
|
||||
/// curve with starting point (0, 0) and ending point (x2, y2).
|
||||
/// [m0] The slope of the line tangent to the curve at (0, 0).
|
||||
/// [m2] The slope of the line tangent to the curve at a different
|
||||
/// point (x2, y2).
|
||||
/// [x2] The x-coordinate of the other point on the curve.
|
||||
/// [y2] The y-coordinate of the other point on the curve.
|
||||
/// Returns a list [:[x1, y1]:] representing the intermediate
|
||||
/// control point of the quadratic Bezier.
|
||||
static List<num> _tangentLinesToQuadraticBezier(
|
||||
num m0, num m2, num x2, num y2) {
|
||||
if (GoogleMath.nearlyEquals(m0, m2)) {
|
||||
|
@ -83,30 +75,26 @@ class BezierPhysics {
|
|||
return [x1, y1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a quadratic Bezier curve to have end point at (1, 1).
|
||||
* [x1] The x-coordinate of the intermediate control point.
|
||||
* [y1] The y-coordinate of the intermediate control point.
|
||||
* [x2] The x-coordinate of the end point.
|
||||
* [y2] The y-coordinate of the end point.
|
||||
* Returns a list [:[x1, y1]:] representing the intermediate control point.
|
||||
*/
|
||||
/// Normalizes a quadratic Bezier curve to have end point at (1, 1).
|
||||
/// [x1] The x-coordinate of the intermediate control point.
|
||||
/// [y1] The y-coordinate of the intermediate control point.
|
||||
/// [x2] The x-coordinate of the end point.
|
||||
/// [y2] The y-coordinate of the end point.
|
||||
/// Returns a list [:[x1, y1]:] representing the intermediate control point.
|
||||
static List<num> _normalizeQuadraticBezier(num x1, num y1, num x2, num y2) {
|
||||
// The end point must not lie on any axes.
|
||||
assert(!GoogleMath.nearlyEquals(x2, 0) && !GoogleMath.nearlyEquals(y2, 0));
|
||||
return [x1 / x2, y1 / y2];
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a quadratic Bezier curve defined by the control points
|
||||
* (x0, y0) = (0, 0), (x1, y1) = (x, y), and (x2, y2) = (1, 1) into an
|
||||
* equivalent cubic Bezier curve with four control points. Note that the start
|
||||
* and end points will be unchanged.
|
||||
* [x] The x-coordinate of the intermediate control point.
|
||||
* [y] The y-coordinate of the intermediate control point.
|
||||
* Returns a list [:[x1, y1, x2, y2]:] containing the two
|
||||
* intermediate points of the equivalent cubic Bezier curve.
|
||||
*/
|
||||
/// Converts a quadratic Bezier curve defined by the control points
|
||||
/// (x0, y0) = (0, 0), (x1, y1) = (x, y), and (x2, y2) = (1, 1) into an
|
||||
/// equivalent cubic Bezier curve with four control points. Note that the start
|
||||
/// and end points will be unchanged.
|
||||
/// [x] The x-coordinate of the intermediate control point.
|
||||
/// [y] The y-coordinate of the intermediate control point.
|
||||
/// Returns a list [:[x1, y1, x2, y2]:] containing the two
|
||||
/// intermediate points of the equivalent cubic Bezier curve.
|
||||
static List<num> _quadraticToCubic(num x, num y) {
|
||||
// The intermediate control point must have coordinates within the
|
||||
// interval [0,1].
|
||||
|
|
|
@ -6,57 +6,47 @@
|
|||
|
||||
part of touch;
|
||||
|
||||
/**
|
||||
* Click buster implementation, which is a behavior that prevents native clicks
|
||||
* from firing at undesirable times. There are two scenarios where we may want
|
||||
* to 'bust' a click.
|
||||
*
|
||||
* Buttons implemented with touch events usually have click handlers as well.
|
||||
* This is because sometimes touch events stop working, and the click handler
|
||||
* serves as a fallback. Here we use a click buster to prevent the native click
|
||||
* from firing if the touchend event was successfully handled.
|
||||
*
|
||||
* When native scrolling behavior is disabled (see Scroller), click events will
|
||||
* fire after the touchend event when the drag sequence is complete. The click
|
||||
* event also happens to fire at the location of the touchstart event which can
|
||||
* lead to some very strange behavior.
|
||||
*
|
||||
* This class puts a single click handler on the body, and calls preventDefault
|
||||
* on the click event if we detect that there was a touchend event that already
|
||||
* fired in the same spot recently.
|
||||
*/
|
||||
/// Click buster implementation, which is a behavior that prevents native clicks
|
||||
/// from firing at undesirable times. There are two scenarios where we may want
|
||||
/// to 'bust' a click.
|
||||
///
|
||||
/// Buttons implemented with touch events usually have click handlers as well.
|
||||
/// This is because sometimes touch events stop working, and the click handler
|
||||
/// serves as a fallback. Here we use a click buster to prevent the native click
|
||||
/// from firing if the touchend event was successfully handled.
|
||||
///
|
||||
/// When native scrolling behavior is disabled (see Scroller), click events will
|
||||
/// fire after the touchend event when the drag sequence is complete. The click
|
||||
/// event also happens to fire at the location of the touchstart event which can
|
||||
/// lead to some very strange behavior.
|
||||
///
|
||||
/// This class puts a single click handler on the body, and calls preventDefault
|
||||
/// on the click event if we detect that there was a touchend event that already
|
||||
/// fired in the same spot recently.
|
||||
class ClickBuster {
|
||||
/**
|
||||
* The threshold for how long we allow a click to occur after a touchstart.
|
||||
*/
|
||||
/// The threshold for how long we allow a click to occur after a touchstart.
|
||||
static const _TIME_THRESHOLD = 2500;
|
||||
|
||||
/**
|
||||
* The threshold for how close a click has to be to the saved coordinate for
|
||||
* us to allow it.
|
||||
*/
|
||||
/// The threshold for how close a click has to be to the saved coordinate for
|
||||
/// us to allow it.
|
||||
static const _DISTANCE_THRESHOLD = 25;
|
||||
|
||||
/**
|
||||
* The list of coordinates that we use to measure the distance of clicks from.
|
||||
* If a click is within the distance threshold of any of these coordinates
|
||||
* then we allow the click.
|
||||
*/
|
||||
/// The list of coordinates that we use to measure the distance of clicks from.
|
||||
/// If a click is within the distance threshold of any of these coordinates
|
||||
/// then we allow the click.
|
||||
static DoubleLinkedQueue<num> _coordinates;
|
||||
|
||||
/** The last time preventGhostClick was called. */
|
||||
/// The last time preventGhostClick was called. */
|
||||
static int _lastPreventedTime;
|
||||
|
||||
/**
|
||||
* This handler will prevent the default behavior for any clicks unless the
|
||||
* click is within the distance threshold of one of the temporary allowed
|
||||
* coordinates.
|
||||
*/
|
||||
/// This handler will prevent the default behavior for any clicks unless the
|
||||
/// click is within the distance threshold of one of the temporary allowed
|
||||
/// coordinates.
|
||||
static void _onClick(Event e) {
|
||||
if (TimeUtil.now() - _lastPreventedTime > _TIME_THRESHOLD) {
|
||||
return;
|
||||
}
|
||||
final coord = new Coordinate.fromClient(e);
|
||||
final coord = Coordinate.fromClient(e);
|
||||
// TODO(rnystrom): On Android, we get spurious click events at (0, 0). We
|
||||
// *do* want those clicks to be busted, so commenting this out fixes it.
|
||||
// Leaving it commented out instead of just deleting it because I'm not sure
|
||||
|
@ -87,33 +77,27 @@ class ClickBuster {
|
|||
e.preventDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
* This handler will temporarily allow a click to occur near the touch event's
|
||||
* coordinates.
|
||||
*/
|
||||
/// This handler will temporarily allow a click to occur near the touch event's
|
||||
/// coordinates.
|
||||
static void _onTouchStart(Event e) {
|
||||
TouchEvent te = e;
|
||||
final coord = new Coordinate.fromClient(te.touches[0]);
|
||||
final coord = Coordinate.fromClient(te.touches[0]);
|
||||
_coordinates.add(coord.x);
|
||||
_coordinates.add(coord.y);
|
||||
new Timer(const Duration(milliseconds: _TIME_THRESHOLD), () {
|
||||
Timer(const Duration(milliseconds: _TIME_THRESHOLD), () {
|
||||
_removeCoordinate(coord.x, coord.y);
|
||||
});
|
||||
_toggleTapHighlights(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hit test for whether a coordinate is within the distance threshold of an
|
||||
* event.
|
||||
*/
|
||||
/// Hit test for whether a coordinate is within the distance threshold of an
|
||||
/// event.
|
||||
static bool _hitTest(num x, num y, num eventX, num eventY) {
|
||||
return (eventX - x).abs() < _DISTANCE_THRESHOLD &&
|
||||
(eventY - y).abs() < _DISTANCE_THRESHOLD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove one specified coordinate from the coordinates list.
|
||||
*/
|
||||
/// Remove one specified coordinate from the coordinates list.
|
||||
static void _removeCoordinate(num x, num y) {
|
||||
DoubleLinkedQueueEntry<num> entry = _coordinates.firstEntry();
|
||||
while (entry != null) {
|
||||
|
@ -127,26 +111,22 @@ class ClickBuster {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable tap highlights. They are disabled when preventGhostClick
|
||||
* is called so that the flicker on links is not invoked when the ghost click
|
||||
* does fire. This is due to a bug: links get highlighted even if the click
|
||||
* event has preventDefault called on it.
|
||||
*/
|
||||
/// Enable or disable tap highlights. They are disabled when preventGhostClick
|
||||
/// is called so that the flicker on links is not invoked when the ghost click
|
||||
/// does fire. This is due to a bug: links get highlighted even if the click
|
||||
/// event has preventDefault called on it.
|
||||
static void _toggleTapHighlights(bool enable) {
|
||||
document.body.style.setProperty(
|
||||
"-webkit-tap-highlight-color", enable ? "" : "rgba(0,0,0,0)", "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers new touches to create temporary "allowable zones" and registers
|
||||
* new clicks to be prevented unless they fall in one of the current
|
||||
* "allowable zones". Note that if the touchstart and touchend locations are
|
||||
* different, it is still possible for a ghost click to be fired if you
|
||||
* called preventDefault on all touchmove events. In this case the ghost
|
||||
* click will be fired at the location of the touchstart event, so the
|
||||
* coordinate you pass in should be the coordinate of the touchstart.
|
||||
*/
|
||||
/// Registers new touches to create temporary "allowable zones" and registers
|
||||
/// new clicks to be prevented unless they fall in one of the current
|
||||
/// "allowable zones". Note that if the touchstart and touchend locations are
|
||||
/// different, it is still possible for a ghost click to be fired if you
|
||||
/// called preventDefault on all touchmove events. In this case the ghost
|
||||
/// click will be fired at the location of the touchstart event, so the
|
||||
/// coordinate you pass in should be the coordinate of the touchstart.
|
||||
static void preventGhostClick(num x, num y) {
|
||||
// First time this is called the following occurs:
|
||||
// 1) Attaches a handler to touchstart events so that each touch will
|
||||
|
@ -196,14 +176,14 @@ class ClickBuster {
|
|||
if (!Device.supportsTouch) {
|
||||
startFn = mouseToTouchCallback(startFn);
|
||||
}
|
||||
var stream;
|
||||
Stream<UIEvent> stream;
|
||||
if (Device.supportsTouch) {
|
||||
stream = Element.touchStartEvent.forTarget(document, useCapture: true);
|
||||
} else {
|
||||
stream = Element.mouseDownEvent.forTarget(document, useCapture: true);
|
||||
}
|
||||
EventUtil.observe(document, stream, startFn, true);
|
||||
_coordinates = new DoubleLinkedQueue<num>();
|
||||
_coordinates = DoubleLinkedQueue<num>();
|
||||
}
|
||||
|
||||
// Turn tap highlights off until we know the ghost click has fired.
|
||||
|
|
|
@ -7,17 +7,13 @@
|
|||
|
||||
part of touch;
|
||||
|
||||
/**
|
||||
* Common events related helpers.
|
||||
*/
|
||||
/// Common events related helpers.
|
||||
class EventUtil {
|
||||
/**
|
||||
* Add an event listener to an element.
|
||||
* The event callback is specified by [handler].
|
||||
* If [capture] is true, the listener gets events on the capture phase.
|
||||
* If [removeHandlerOnFocus] is true the handler is removed when there is any
|
||||
* focus event, and added back on blur events.
|
||||
*/
|
||||
/// Add an event listener to an element.
|
||||
/// The event callback is specified by [handler].
|
||||
/// If [capture] is true, the listener gets events on the capture phase.
|
||||
/// If [removeHandlerOnFocus] is true the handler is removed when there is any
|
||||
/// focus event, and added back on blur events.
|
||||
static void observe(
|
||||
/*Element or Document*/ element, Stream stream, Function handler,
|
||||
[bool removeHandlerOnFocus = false]) {
|
||||
|
@ -33,11 +29,9 @@ class EventUtil {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the keyboard focus of the currently focused element (if there is
|
||||
* one). If there is no currently focused element then this function will do
|
||||
* nothing. For most browsers this will cause the keyboard to be dismissed.
|
||||
*/
|
||||
/// Clear the keyboard focus of the currently focused element (if there is
|
||||
/// one). If there is no currently focused element then this function will do
|
||||
/// nothing. For most browsers this will cause the keyboard to be dismissed.
|
||||
static void blurFocusedElement() {
|
||||
Element focusedEl = document.querySelector("*:focus");
|
||||
if (focusedEl != null) {
|
||||
|
|
|
@ -6,23 +6,21 @@
|
|||
|
||||
part of touch;
|
||||
|
||||
/**
|
||||
* Common effects related helpers.
|
||||
*/
|
||||
/// Common effects related helpers.
|
||||
class FxUtil {
|
||||
/** On transition end event. */
|
||||
/// On transition end event. */
|
||||
static const TRANSITION_END_EVENT = 'webkitTransitionEnd';
|
||||
|
||||
/** The translate3d transform function. */
|
||||
/// The translate3d transform function. */
|
||||
static const TRANSLATE_3D = 'translate3d';
|
||||
|
||||
/** The rotate transform function. */
|
||||
/// The rotate transform function. */
|
||||
static const ROTATE = 'rotate';
|
||||
|
||||
/** The scale transform function. */
|
||||
/// The scale transform function. */
|
||||
static const SCALE = 'scale';
|
||||
|
||||
/** Stops and clears the transition on an element. */
|
||||
/// Stops and clears the transition on an element. */
|
||||
static void clearWebkitTransition(Element el) {
|
||||
el.style.transition = '';
|
||||
}
|
||||
|
@ -30,31 +28,27 @@ class FxUtil {
|
|||
static void setPosition(Element el, Coordinate point) {
|
||||
num x = point.x;
|
||||
num y = point.y;
|
||||
el.style.transform = '${TRANSLATE_3D}(${x}px,${y}px,0px)';
|
||||
el.style.transform = '$TRANSLATE_3D(${x}px,${y}px,0px)';
|
||||
}
|
||||
|
||||
/** Apply a transform using translate3d to an HTML element. */
|
||||
/// Apply a transform using translate3d to an HTML element. */
|
||||
static void setTranslate(Element el, num x, num y, num z) {
|
||||
el.style.transform = '${TRANSLATE_3D}(${x}px,${y}px,${z}px)';
|
||||
el.style.transform = '$TRANSLATE_3D(${x}px,${y}px,${z}px)';
|
||||
}
|
||||
|
||||
/** Apply a -webkit-transform using translate3d to an HTML element. */
|
||||
/// Apply a -webkit-transform using translate3d to an HTML element. */
|
||||
static void setWebkitTransform(Element el, num x, num y,
|
||||
[num z = 0,
|
||||
num rotation = null,
|
||||
num scale = null,
|
||||
num originX = null,
|
||||
num originY = null]) {
|
||||
[num z = 0, num rotation, num scale, num originX, num originY]) {
|
||||
final style = el.style;
|
||||
// TODO(jacobr): create a helper class that simplifies building
|
||||
// transformation matricies that will be set as CSS styles. We should
|
||||
// consider using CSSMatrix although that may be overkill.
|
||||
String transform = '${TRANSLATE_3D}(${x}px,${y}px,${z}px)';
|
||||
String transform = '$TRANSLATE_3D(${x}px,${y}px,${z}px)';
|
||||
if (rotation != null) {
|
||||
transform += ' ${ROTATE}(${rotation}deg)';
|
||||
transform += ' $ROTATE(${rotation}deg)';
|
||||
}
|
||||
if (scale != null) {
|
||||
transform += ' ${SCALE}(${scale})';
|
||||
transform += ' $SCALE($scale)';
|
||||
}
|
||||
style.transform = transform;
|
||||
if (originX != null || originY != null) {
|
||||
|
@ -63,15 +57,13 @@ class FxUtil {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the position of an [element] relative to a [target] element.
|
||||
* Moving the [element] to be a child of [target] and setting the
|
||||
* [element]'s top and left values to the returned coordinate should result
|
||||
* in the [element]'s position remaining unchanged while its parent is
|
||||
* changed.
|
||||
*/
|
||||
/// Determine the position of an [element] relative to a [target] element.
|
||||
/// Moving the [element] to be a child of [target] and setting the
|
||||
/// [element]'s top and left values to the returned coordinate should result
|
||||
/// in the [element]'s position remaining unchanged while its parent is
|
||||
/// changed.
|
||||
static Coordinate computeRelativePosition(Element element, Element target) {
|
||||
final testPoint = new Point(0, 0);
|
||||
final testPoint = Point(0, 0);
|
||||
/*
|
||||
final pagePoint =
|
||||
window.convertPointFromNodeToPage(element, testPoint);
|
||||
|
@ -83,25 +75,21 @@ class FxUtil {
|
|||
// `convertPointFromPageToNode`.
|
||||
var eRect = element.getBoundingClientRect();
|
||||
var tRect = target.getBoundingClientRect();
|
||||
return new Coordinate(eRect.left - tRect.left, eRect.top - tRect.top);
|
||||
return Coordinate(eRect.left - tRect.left, eRect.top - tRect.top);
|
||||
}
|
||||
|
||||
/** Clear a -webkit-transform from an element. */
|
||||
/// Clear a -webkit-transform from an element. */
|
||||
static void clearWebkitTransform(Element el) {
|
||||
el.style.transform = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether an element has a translate3d webkit transform applied.
|
||||
*/
|
||||
/// Checks whether an element has a translate3d webkit transform applied.
|
||||
static bool hasWebkitTransform(Element el) {
|
||||
return el.style.transform.indexOf(TRANSLATE_3D, 0) != -1;
|
||||
return el.style.transform.contains(TRANSLATE_3D, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates [el], an HTML element that has a relative CSS
|
||||
* position, by setting its left and top CSS styles.
|
||||
*/
|
||||
/// Translates [el], an HTML element that has a relative CSS
|
||||
/// position, by setting its left and top CSS styles.
|
||||
static void setLeftAndTop(Element el, num x, num y) {
|
||||
final style = el.style;
|
||||
style.left = '${x}px';
|
||||
|
|
|
@ -6,30 +6,22 @@
|
|||
|
||||
part of touch;
|
||||
|
||||
/**
|
||||
* Represents a point in 2 dimensional space.
|
||||
*/
|
||||
/// Represents a point in 2 dimensional space.
|
||||
class Coordinate {
|
||||
/**
|
||||
* X-value
|
||||
*/
|
||||
/// X-value
|
||||
num x;
|
||||
|
||||
/**
|
||||
* Y-value
|
||||
*/
|
||||
/// Y-value
|
||||
num y;
|
||||
|
||||
Coordinate([num this.x = 0, num this.y = 0]) {}
|
||||
Coordinate([this.x = 0, this.y = 0]);
|
||||
|
||||
/**
|
||||
* Gets the coordinates of a touch's location relative to the window's
|
||||
* viewport. [input] is either a touch object or an event object.
|
||||
*/
|
||||
/// Gets the coordinates of a touch's location relative to the window's
|
||||
/// viewport. [input] is either a touch object or an event object.
|
||||
Coordinate.fromClient(var input) : this(input.client.x, input.client.y);
|
||||
|
||||
static Coordinate difference(Coordinate a, Coordinate b) {
|
||||
return new Coordinate(a.x - b.x, a.y - b.y);
|
||||
return Coordinate(a.x - b.x, a.y - b.y);
|
||||
}
|
||||
|
||||
static num distance(Coordinate a, Coordinate b) {
|
||||
|
@ -38,11 +30,13 @@ class Coordinate {
|
|||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant Coordinate other) {
|
||||
return other != null && x == other.x && y == other.y;
|
||||
return x == other.x && y == other.y;
|
||||
}
|
||||
|
||||
int get hashCode => throw new UnimplementedError();
|
||||
@override
|
||||
int get hashCode => throw UnimplementedError();
|
||||
|
||||
static num squaredDistance(Coordinate a, Coordinate b) {
|
||||
final dx = a.x - b.x;
|
||||
|
@ -51,45 +45,45 @@ class Coordinate {
|
|||
}
|
||||
|
||||
static Coordinate sum(Coordinate a, Coordinate b) {
|
||||
return new Coordinate(a.x + b.x, a.y + b.y);
|
||||
return Coordinate(a.x + b.x, a.y + b.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new copy of the coordinate.
|
||||
*/
|
||||
Coordinate clone() => new Coordinate(x, y);
|
||||
/// Returns a new copy of the coordinate.
|
||||
Coordinate clone() => Coordinate(x, y);
|
||||
|
||||
@override
|
||||
String toString() => "($x, $y)";
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the interval { x | start <= x < end }.
|
||||
*/
|
||||
/// Represents the interval { x | start <= x < end }.
|
||||
class Interval {
|
||||
final num start;
|
||||
final num end;
|
||||
|
||||
Interval(num this.start, num this.end) {}
|
||||
Interval(this.start, this.end);
|
||||
|
||||
num get length {
|
||||
return end - start;
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(covariant Interval other) {
|
||||
return other != null && other.start == start && other.end == end;
|
||||
return other.start == start && other.end == end;
|
||||
}
|
||||
|
||||
int get hashCode => throw new UnimplementedError();
|
||||
@override
|
||||
int get hashCode => throw UnimplementedError();
|
||||
|
||||
Interval union(Interval other) {
|
||||
return new Interval(Math.min(start, other.start), Math.max(end, other.end));
|
||||
return Interval(Math.min(start, other.start), Math.max(end, other.end));
|
||||
}
|
||||
|
||||
bool contains(num value) {
|
||||
return value >= start && value < end;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '(${start}, ${end})';
|
||||
return '($start, $end)';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,50 +6,44 @@
|
|||
|
||||
part of touch;
|
||||
|
||||
/**
|
||||
* Adds a listener to the scroller with triggers events
|
||||
* when a trigger point at the top, or bottom, of the screen is reached.
|
||||
*
|
||||
* To use this you will need to have an element with a scroller attached
|
||||
* to it. You need to have defined (in pixels) how far from the top or
|
||||
* bottom the scroll position must be in order to trigger (the "trigger
|
||||
* point") The element using this must have functions for hitting the top
|
||||
* trigger, and the bottom trigger. In general, these methods will
|
||||
* ascertain whether we have more data to scroll to (i.e. when we hit
|
||||
* the bottom trigger point but have reached the end of the data
|
||||
* displayed in the element we should ignore it), make the call for
|
||||
* more data and reposition the scroller - repositioning is key to
|
||||
* good user experience.
|
||||
*
|
||||
* Triggers are generated by listening for the SCROLL_END event from the
|
||||
* scroller, so data calls are not initiated whilst scrolling is happening,
|
||||
* but after.
|
||||
*
|
||||
* Controls changing divs between the usual (non-loading) div and the
|
||||
* loading div. To take advantage of this, callback function should return
|
||||
* a boolean indicating whether the usual div should be replaced by the
|
||||
* loading div.
|
||||
*/
|
||||
/// Adds a listener to the scroller with triggers events
|
||||
/// when a trigger point at the top, or bottom, of the screen is reached.
|
||||
///
|
||||
/// To use this you will need to have an element with a scroller attached
|
||||
/// to it. You need to have defined (in pixels) how far from the top or
|
||||
/// bottom the scroll position must be in order to trigger (the "trigger
|
||||
/// point") The element using this must have functions for hitting the top
|
||||
/// trigger, and the bottom trigger. In general, these methods will
|
||||
/// ascertain whether we have more data to scroll to (i.e. when we hit
|
||||
/// the bottom trigger point but have reached the end of the data
|
||||
/// displayed in the element we should ignore it), make the call for
|
||||
/// more data and reposition the scroller - repositioning is key to
|
||||
/// good user experience.
|
||||
///
|
||||
/// Triggers are generated by listening for the SCROLL_END event from the
|
||||
/// scroller, so data calls are not initiated whilst scrolling is happening,
|
||||
/// but after.
|
||||
///
|
||||
/// Controls changing divs between the usual (non-loading) div and the
|
||||
/// loading div. To take advantage of this, callback function should return
|
||||
/// a boolean indicating whether the usual div should be replaced by the
|
||||
/// loading div.
|
||||
class InfiniteScroller {
|
||||
Scroller _scroller;
|
||||
final Scroller _scroller;
|
||||
|
||||
/**
|
||||
* Function to invoke when trigger point is reached at the top of the view.
|
||||
*/
|
||||
Function _onTopScroll;
|
||||
/// Function to invoke when trigger point is reached at the top of the view.
|
||||
final Function _onTopScroll;
|
||||
|
||||
/**
|
||||
* Function to invoke when trigger point is reached at the bottom of the view.
|
||||
*/
|
||||
Function _onBottomScroll;
|
||||
/// Function to invoke when trigger point is reached at the bottom of the view.
|
||||
final Function _onBottomScroll;
|
||||
|
||||
/** Offset for trigger point at the top of the view. */
|
||||
double _offsetTop;
|
||||
/// Offset for trigger point at the top of the view. */
|
||||
final double _offsetTop;
|
||||
|
||||
/** Offset for trigger point at the bottom of the view. */
|
||||
double _offsetBottom;
|
||||
/// Offset for trigger point at the bottom of the view. */
|
||||
final double _offsetBottom;
|
||||
|
||||
/** Saves the last Y position. */
|
||||
/// Saves the last Y position. */
|
||||
double _lastScrollY;
|
||||
Element _topDiv;
|
||||
Element _topLoadingDiv;
|
||||
|
@ -58,28 +52,26 @@ class InfiniteScroller {
|
|||
|
||||
InfiniteScroller(Scroller scroller, Function onTopScroll,
|
||||
Function onBottomScroll, double offsetTop,
|
||||
[double offsetBottom = null])
|
||||
[double offsetBottom])
|
||||
: _scroller = scroller,
|
||||
_onTopScroll = onTopScroll,
|
||||
_onBottomScroll = onBottomScroll,
|
||||
_offsetTop = offsetTop,
|
||||
_offsetBottom = offsetBottom == null ? offsetTop : offsetBottom,
|
||||
_lastScrollY = 0.0 {}
|
||||
_offsetBottom = offsetBottom ?? offsetTop,
|
||||
_lastScrollY = 0.0;
|
||||
|
||||
/**
|
||||
* Adds the loading divs.
|
||||
* [topDiv] The div usually shown at the top.
|
||||
* [topLoadingDiv] is the div to show at the top when waiting for more
|
||||
* content to load at the top of the page.
|
||||
* [bottomDiv] is the div usually shown at the bottom.
|
||||
* [bottomLoadingDiv] is the div to show at the bottom when waiting for more
|
||||
* content to load at the end of the page.
|
||||
*/
|
||||
/// Adds the loading divs.
|
||||
/// [topDiv] The div usually shown at the top.
|
||||
/// [topLoadingDiv] is the div to show at the top when waiting for more
|
||||
/// content to load at the top of the page.
|
||||
/// [bottomDiv] is the div usually shown at the bottom.
|
||||
/// [bottomLoadingDiv] is the div to show at the bottom when waiting for more
|
||||
/// content to load at the end of the page.
|
||||
void addLoadingDivs(
|
||||
[Element topDiv = null,
|
||||
Element topLoadingDiv = null,
|
||||
Element bottomDiv = null,
|
||||
Element bottomLoadingDiv = null]) {
|
||||
[Element topDiv,
|
||||
Element topLoadingDiv,
|
||||
Element bottomDiv,
|
||||
Element bottomLoadingDiv]) {
|
||||
_topDiv = topDiv;
|
||||
_topLoadingDiv = topLoadingDiv;
|
||||
_bottomDiv = bottomDiv;
|
||||
|
@ -92,18 +84,14 @@ class InfiniteScroller {
|
|||
_registerEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch back the divs after loading complete. Delegate should call
|
||||
* this function after loading is complete.
|
||||
*/
|
||||
/// Switch back the divs after loading complete. Delegate should call
|
||||
/// this function after loading is complete.
|
||||
void loadEnd() {
|
||||
_updateVisibility(false, _topDiv, _topLoadingDiv);
|
||||
_updateVisibility(false, _bottomDiv, _bottomLoadingDiv);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called at the end of a scroll event.
|
||||
*/
|
||||
/// Called at the end of a scroll event.
|
||||
void _onScrollEnd() {
|
||||
double ypos = _scroller.getVerticalOffset();
|
||||
|
||||
|
@ -127,18 +115,14 @@ class InfiniteScroller {
|
|||
_lastScrollY = ypos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the event listeners.
|
||||
*/
|
||||
/// Register the event listeners.
|
||||
void _registerEventListeners() {
|
||||
_scroller.onScrollerEnd.listen((Event event) {
|
||||
_onScrollEnd();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hides one div and shows another.
|
||||
*/
|
||||
/// Hides one div and shows another.
|
||||
void _updateVisibility(
|
||||
bool isLoading, Element element, Element loadingElement) {
|
||||
if (element != null) {
|
||||
|
|
|
@ -7,25 +7,19 @@
|
|||
part of touch;
|
||||
|
||||
// TODO(jacobr): pick a better name. This was goog.math in Closure.
|
||||
/**
|
||||
* Math utility functions originally from the closure Math library.
|
||||
*/
|
||||
/// Math utility functions originally from the closure Math library.
|
||||
class GoogleMath {
|
||||
/**
|
||||
* Takes a [value] and clamps it to within the bounds specified by
|
||||
* [min] and [max].
|
||||
*/
|
||||
/// Takes a [value] and clamps it to within the bounds specified by
|
||||
/// [min] and [max].
|
||||
static num clamp(num value, num min, num max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether the two values are equal to each other, within a certain
|
||||
* tolerance to adjust for floating point errors.
|
||||
* The optional [tolerance] value d Defaults to 0.000001. If specified,
|
||||
* it should be greater than 0.
|
||||
* Returns whether [a] and [b] are nearly equal.
|
||||
*/
|
||||
/// Tests whether the two values are equal to each other, within a certain
|
||||
/// tolerance to adjust for floating point errors.
|
||||
/// The optional [tolerance] value d Defaults to 0.000001. If specified,
|
||||
/// it should be greater than 0.
|
||||
/// Returns whether [a] and [b] are nearly equal.
|
||||
static bool nearlyEquals(num a, num b, [num tolerance = 0.000001]) {
|
||||
return (a - b).abs() <= tolerance;
|
||||
}
|
||||
|
|
|
@ -6,31 +6,29 @@
|
|||
|
||||
part of touch;
|
||||
|
||||
/**
|
||||
* Implementations can be used to simulate the deceleration of an element within
|
||||
* a certain region. To use this behavior you need to provide an initial
|
||||
* velocity that is meant to represent the gesture that is initiating this
|
||||
* deceleration. You also provide the bounds of the region that the element
|
||||
* exists in, and the current offset of the element within that region. The
|
||||
* transitions will have the element decelerate to rest, or stretch past the
|
||||
* offset boundaries and then come to rest.
|
||||
*
|
||||
* This is primarily designed to solve the problem of slow scrolling in mobile
|
||||
* safari. You can use this along with the [Scroller] behavior to make a
|
||||
* scrollable area scroll the same way it would in a native application.
|
||||
*
|
||||
* Implementations of this interface do not maintain any references to HTML
|
||||
* elements, and therefore cannot do any redrawing of elements. They only
|
||||
* calculates where the element should be on an interval. It is the delegate's
|
||||
* responsibility to redraw the element when the onDecelerate callback is
|
||||
* invoked. It is recommended that you move the element with a hardware
|
||||
* accelerated method such as using 'translate3d' on the element's
|
||||
* -webkit-transform style property.
|
||||
*/
|
||||
/// Implementations can be used to simulate the deceleration of an element within
|
||||
/// a certain region. To use this behavior you need to provide an initial
|
||||
/// velocity that is meant to represent the gesture that is initiating this
|
||||
/// deceleration. You also provide the bounds of the region that the element
|
||||
/// exists in, and the current offset of the element within that region. The
|
||||
/// transitions will have the element decelerate to rest, or stretch past the
|
||||
/// offset boundaries and then come to rest.
|
||||
///
|
||||
/// This is primarily designed to solve the problem of slow scrolling in mobile
|
||||
/// safari. You can use this along with the [Scroller] behavior to make a
|
||||
/// scrollable area scroll the same way it would in a native application.
|
||||
///
|
||||
/// Implementations of this interface do not maintain any references to HTML
|
||||
/// elements, and therefore cannot do any redrawing of elements. They only
|
||||
/// calculates where the element should be on an interval. It is the delegate's
|
||||
/// responsibility to redraw the element when the onDecelerate callback is
|
||||
/// invoked. It is recommended that you move the element with a hardware
|
||||
/// accelerated method such as using 'translate3d' on the element's
|
||||
/// -webkit-transform style property.
|
||||
abstract class Momentum {
|
||||
factory Momentum(MomentumDelegate delegate,
|
||||
[num defaultDecelerationFactor = 1]) =>
|
||||
new TimeoutMomentum(delegate, defaultDecelerationFactor);
|
||||
TimeoutMomentum(delegate, defaultDecelerationFactor);
|
||||
|
||||
bool get decelerating;
|
||||
|
||||
|
@ -42,50 +40,40 @@ abstract class Momentum {
|
|||
*/
|
||||
void onTransitionEnd();
|
||||
|
||||
/**
|
||||
* Start decelerating.
|
||||
* The [velocity] passed should be in terms of number of pixels / millisecond.
|
||||
* [minCoord] and [maxCoord] specify the content's scrollable boundary.
|
||||
* The current offset of the element within its boundaries is specified by
|
||||
* [initialOffset].
|
||||
* Returns true if deceleration has been initiated.
|
||||
*/
|
||||
/// Start decelerating.
|
||||
/// The [velocity] passed should be in terms of number of pixels / millisecond.
|
||||
/// [minCoord] and [maxCoord] specify the content's scrollable boundary.
|
||||
/// The current offset of the element within its boundaries is specified by
|
||||
/// [initialOffset].
|
||||
/// Returns true if deceleration has been initiated.
|
||||
bool start(Coordinate velocity, Coordinate minCoord, Coordinate maxCoord,
|
||||
Coordinate initialOffset,
|
||||
[num decelerationFactor]);
|
||||
|
||||
/**
|
||||
* Calculate the velocity required to transition between coordinates [start]
|
||||
* and [target] optionally specifying a custom [decelerationFactor].
|
||||
*/
|
||||
/// Calculate the velocity required to transition between coordinates [start]
|
||||
/// and [target] optionally specifying a custom [decelerationFactor].
|
||||
Coordinate calculateVelocity(Coordinate start, Coordinate target,
|
||||
[num decelerationFactor]);
|
||||
|
||||
/** Stop decelerating and return the current velocity. */
|
||||
/// Stop decelerating and return the current velocity. */
|
||||
Coordinate stop();
|
||||
|
||||
/** Aborts decelerating without dispatching any notification events. */
|
||||
/// Aborts decelerating without dispatching any notification events. */
|
||||
void abort();
|
||||
|
||||
/** null if no transition is in progress. */
|
||||
/// null if no transition is in progress. */
|
||||
Coordinate get destination;
|
||||
}
|
||||
|
||||
/**
|
||||
* Momentum Delegate interface.
|
||||
* You are required to implement this interface in order to use the
|
||||
* Momentum behavior.
|
||||
*/
|
||||
/// Momentum Delegate interface.
|
||||
/// You are required to implement this interface in order to use the
|
||||
/// Momentum behavior.
|
||||
abstract class MomentumDelegate {
|
||||
/**
|
||||
* Callback for a deceleration step. The delegate is responsible for redrawing
|
||||
* the element in its new position specified in px.
|
||||
*/
|
||||
/// Callback for a deceleration step. The delegate is responsible for redrawing
|
||||
/// the element in its new position specified in px.
|
||||
void onDecelerate(num x, num y);
|
||||
|
||||
/**
|
||||
* Callback for end of deceleration.
|
||||
*/
|
||||
/// Callback for end of deceleration.
|
||||
void onDecelerationEnd();
|
||||
}
|
||||
|
||||
|
@ -105,14 +93,12 @@ class _Move {
|
|||
_Move(this.x, this.y, this.vx, this.vy, this.time);
|
||||
}
|
||||
|
||||
/**
|
||||
* Secant method root solver helper class.
|
||||
* We use http://en.wikipedia.org/wiki/Secant_method
|
||||
* falling back to the http://en.wikipedia.org/wiki/Bisection_method
|
||||
* if it doesn't appear we are converging properlty.
|
||||
* TODO(jacobr): simplify the code so we don't have to use this solver
|
||||
* class at all.
|
||||
*/
|
||||
/// Secant method root solver helper class.
|
||||
/// We use http://en.wikipedia.org/wiki/Secant_method
|
||||
/// falling back to the http://en.wikipedia.org/wiki/Bisection_method
|
||||
/// if it doesn't appear we are converging properlty.
|
||||
/// TODO(jacobr): simplify the code so we don't have to use this solver
|
||||
/// class at all.
|
||||
class Solver {
|
||||
static num solve(num Function(num) fn, num targetY, num startX,
|
||||
[int maxIterations = 50]) {
|
||||
|
@ -120,8 +106,8 @@ class Solver {
|
|||
num lastY = fn(lastX);
|
||||
num deltaX;
|
||||
num deltaY;
|
||||
num minX = null;
|
||||
num maxX = null;
|
||||
num minX;
|
||||
num maxX;
|
||||
num x = startX;
|
||||
num delta = startX;
|
||||
for (int i = 0; i < maxIterations; i++) {
|
||||
|
@ -151,94 +137,74 @@ class Solver {
|
|||
x = (minX + maxX) / 2;
|
||||
}
|
||||
}
|
||||
window.console.warn('''Could not find an exact solution. LastY=${lastY},
|
||||
targetY=${targetY} lastX=$lastX delta=$delta deltaX=$deltaX
|
||||
window.console.warn('''Could not find an exact solution. LastY=$lastY,
|
||||
targetY=$targetY lastX=$lastX delta=$delta deltaX=$deltaX
|
||||
deltaY=$deltaY''');
|
||||
return x;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class modeling the physics of a throwable scrollable area along a
|
||||
* single dimension.
|
||||
*/
|
||||
/// Helper class modeling the physics of a throwable scrollable area along a
|
||||
/// single dimension.
|
||||
class SingleDimensionPhysics {
|
||||
/** The number of frames per second the animation should run at. */
|
||||
/// The number of frames per second the animation should run at. */
|
||||
static const _FRAMES_PER_SECOND = 60;
|
||||
|
||||
/**
|
||||
* The spring coefficient for when the element has passed a boundary and is
|
||||
* decelerating to change direction and bounce back. Each frame, the velocity
|
||||
* will be changed by x times this coefficient, where x is the current stretch
|
||||
* value of the element from its boundary. This will end when velocity reaches
|
||||
* zero.
|
||||
*/
|
||||
/// The spring coefficient for when the element has passed a boundary and is
|
||||
/// decelerating to change direction and bounce back. Each frame, the velocity
|
||||
/// will be changed by x times this coefficient, where x is the current stretch
|
||||
/// value of the element from its boundary. This will end when velocity reaches
|
||||
/// zero.
|
||||
static const _PRE_BOUNCE_COEFFICIENT = 7.0 / _FRAMES_PER_SECOND;
|
||||
|
||||
/**
|
||||
* The spring coefficient for when the element is bouncing back from a
|
||||
* stretched offset to a min or max position. Each frame, the velocity will
|
||||
* be changed to x times this coefficient, where x is the current stretch
|
||||
* value of the element from its boundary. This will end when the stretch
|
||||
* value reaches 0.
|
||||
*/
|
||||
/// The spring coefficient for when the element is bouncing back from a
|
||||
/// stretched offset to a min or max position. Each frame, the velocity will
|
||||
/// be changed to x times this coefficient, where x is the current stretch
|
||||
/// value of the element from its boundary. This will end when the stretch
|
||||
/// value reaches 0.
|
||||
static const _POST_BOUNCE_COEFFICIENT = 7.0 / _FRAMES_PER_SECOND;
|
||||
|
||||
/**
|
||||
* The number of milliseconds per animation frame.
|
||||
*/
|
||||
/// The number of milliseconds per animation frame.
|
||||
static const _MS_PER_FRAME = 1000.0 / _FRAMES_PER_SECOND;
|
||||
|
||||
/**
|
||||
* The constant factor applied to velocity at each frame to simulate
|
||||
* deceleration.
|
||||
*/
|
||||
/// The constant factor applied to velocity at each frame to simulate
|
||||
/// deceleration.
|
||||
static const _DECELERATION_FACTOR = 0.97;
|
||||
|
||||
static const _MAX_VELOCITY_STATIC_FRICTION = 0.08 * _MS_PER_FRAME;
|
||||
static const _DECELERATION_FACTOR_STATIC_FRICTION = 0.92;
|
||||
|
||||
/**
|
||||
* Minimum velocity required to start or continue deceleration, in
|
||||
* pixels/frame. This is equivalent to 0.25 px/ms.
|
||||
*/
|
||||
/// Minimum velocity required to start or continue deceleration, in
|
||||
/// pixels/frame. This is equivalent to 0.25 px/ms.
|
||||
static const _MIN_VELOCITY = 0.25 * _MS_PER_FRAME;
|
||||
|
||||
/**
|
||||
* Minimum velocity during a step, in pixels/frame. This is equivalent to 0.01
|
||||
* px/ms.
|
||||
*/
|
||||
/// Minimum velocity during a step, in pixels/frame. This is equivalent to 0.01
|
||||
/// px/ms.
|
||||
static const _MIN_STEP_VELOCITY = 0.01 * _MS_PER_FRAME;
|
||||
|
||||
/**
|
||||
* Boost the initial velocity by a certain factor before applying momentum.
|
||||
* This just gives the momentum a better feel.
|
||||
*/
|
||||
/// Boost the initial velocity by a certain factor before applying momentum.
|
||||
/// This just gives the momentum a better feel.
|
||||
static const _INITIAL_VELOCITY_BOOST_FACTOR = 1.25;
|
||||
|
||||
/**
|
||||
* Additional deceleration factor to apply for the current move only. This
|
||||
* is helpful for cases such as scroll wheel scrolling where the default
|
||||
* amount of deceleration is inadequate.
|
||||
*/
|
||||
/// Additional deceleration factor to apply for the current move only. This
|
||||
/// is helpful for cases such as scroll wheel scrolling where the default
|
||||
/// amount of deceleration is inadequate.
|
||||
num customDecelerationFactor = 1;
|
||||
num _minCoord;
|
||||
num _maxCoord;
|
||||
|
||||
/** The bouncing state. */
|
||||
/// The bouncing state. */
|
||||
int _bouncingState;
|
||||
|
||||
num velocity;
|
||||
num _currentOffset;
|
||||
|
||||
/**
|
||||
* constant used when guessing at the velocity required to throw to a specific
|
||||
* location. Chosen arbitrarily. All that really matters is that the velocity
|
||||
* is large enough that a throw gesture will occur.
|
||||
*/
|
||||
/// constant used when guessing at the velocity required to throw to a specific
|
||||
/// location. Chosen arbitrarily. All that really matters is that the velocity
|
||||
/// is large enough that a throw gesture will occur.
|
||||
static const _VELOCITY_GUESS = 20;
|
||||
|
||||
SingleDimensionPhysics() : _bouncingState = BouncingState.NOT_BOUNCING {}
|
||||
SingleDimensionPhysics() : _bouncingState = BouncingState.NOT_BOUNCING;
|
||||
|
||||
void configure(num minCoord, num maxCoord, num initialOffset,
|
||||
num customDecelerationFactor_, num velocity_) {
|
||||
|
@ -246,7 +212,7 @@ class SingleDimensionPhysics {
|
|||
_minCoord = minCoord;
|
||||
_maxCoord = maxCoord;
|
||||
_currentOffset = initialOffset;
|
||||
this.customDecelerationFactor = customDecelerationFactor_;
|
||||
customDecelerationFactor = customDecelerationFactor_;
|
||||
_adjustInitialVelocityAndBouncingState(velocity_);
|
||||
}
|
||||
|
||||
|
@ -268,11 +234,9 @@ class SingleDimensionPhysics {
|
|||
targetOffset > initialOffset ? _VELOCITY_GUESS : -_VELOCITY_GUESS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to calculate initial velocity.
|
||||
* The [velocity] passed here should be in terms of number of
|
||||
* pixels / millisecond. Returns the adjusted x and y velocities.
|
||||
*/
|
||||
/// Helper method to calculate initial velocity.
|
||||
/// The [velocity] passed here should be in terms of number of
|
||||
/// pixels / millisecond. Returns the adjusted x and y velocities.
|
||||
void _adjustInitialVelocityAndBouncingState(num v) {
|
||||
velocity = v * _MS_PER_FRAME * _INITIAL_VELOCITY_BOOST_FACTOR;
|
||||
|
||||
|
@ -289,9 +253,7 @@ class SingleDimensionPhysics {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply deceleration.
|
||||
*/
|
||||
/// Apply deceleration.
|
||||
void _adjustVelocity() {
|
||||
num speed = velocity.abs();
|
||||
velocity *= _DECELERATION_FACTOR;
|
||||
|
@ -346,10 +308,8 @@ class SingleDimensionPhysics {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the current velocity is above the threshold required to
|
||||
* continue decelerating.
|
||||
*/
|
||||
/// Whether or not the current velocity is above the threshold required to
|
||||
/// continue decelerating.
|
||||
bool isVelocityAboveThreshold(num threshold) {
|
||||
return velocity.abs() >= threshold;
|
||||
}
|
||||
|
@ -360,35 +320,31 @@ class SingleDimensionPhysics {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of a momentum strategy using webkit-transforms
|
||||
* and timeouts.
|
||||
*/
|
||||
/// Implementation of a momentum strategy using webkit-transforms
|
||||
/// and timeouts.
|
||||
class TimeoutMomentum implements Momentum {
|
||||
SingleDimensionPhysics physicsX;
|
||||
SingleDimensionPhysics physicsY;
|
||||
Coordinate _previousOffset;
|
||||
Queue<_Move> _moves;
|
||||
final Queue<_Move> _moves;
|
||||
num _stepTimeout;
|
||||
bool _decelerating;
|
||||
MomentumDelegate _delegate;
|
||||
final MomentumDelegate _delegate;
|
||||
int _nextY;
|
||||
int _nextX;
|
||||
Coordinate _minCoord;
|
||||
Coordinate _maxCoord;
|
||||
num _customDecelerationFactor;
|
||||
num _defaultDecelerationFactor;
|
||||
final num _defaultDecelerationFactor;
|
||||
|
||||
TimeoutMomentum(this._delegate, [num defaultDecelerationFactor = 1])
|
||||
: _defaultDecelerationFactor = defaultDecelerationFactor,
|
||||
_decelerating = false,
|
||||
_moves = new Queue<_Move>(),
|
||||
physicsX = new SingleDimensionPhysics(),
|
||||
physicsY = new SingleDimensionPhysics();
|
||||
_moves = Queue<_Move>(),
|
||||
physicsX = SingleDimensionPhysics(),
|
||||
physicsY = SingleDimensionPhysics();
|
||||
|
||||
/**
|
||||
* Calculate and return the moves for the deceleration motion.
|
||||
*/
|
||||
/// Calculate and return the moves for the deceleration motion.
|
||||
void _calculateMoves() {
|
||||
_moves.clear();
|
||||
num time = TimeUtil.now();
|
||||
|
@ -396,43 +352,43 @@ class TimeoutMomentum implements Momentum {
|
|||
_stepWithoutAnimation();
|
||||
time += SingleDimensionPhysics._MS_PER_FRAME;
|
||||
if (_isStepNecessary()) {
|
||||
_moves.add(new _Move(
|
||||
_nextX, _nextY, physicsX.velocity, physicsY.velocity, time));
|
||||
_moves.add(
|
||||
_Move(_nextX, _nextY, physicsX.velocity, physicsY.velocity, time));
|
||||
_previousOffset.y = _nextY;
|
||||
_previousOffset.x = _nextX;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool get decelerating => _decelerating;
|
||||
@override
|
||||
num get decelerationFactor => _customDecelerationFactor;
|
||||
|
||||
/**
|
||||
* Checks whether or not an animation step is necessary or not. Animations
|
||||
* steps are not necessary when the velocity gets so low that in several
|
||||
* frames the offset is the same.
|
||||
* Returns true if there is movement to be done in the next frame.
|
||||
*/
|
||||
/// Checks whether or not an animation step is necessary or not. Animations
|
||||
/// steps are not necessary when the velocity gets so low that in several
|
||||
/// frames the offset is the same.
|
||||
/// Returns true if there is movement to be done in the next frame.
|
||||
bool _isStepNecessary() {
|
||||
return _nextY != _previousOffset.y || _nextX != _previousOffset.x;
|
||||
}
|
||||
|
||||
/**
|
||||
* The [TouchHandler] requires this function but we don't need to do
|
||||
* anything here.
|
||||
*/
|
||||
/// The [TouchHandler] requires this function but we don't need to do
|
||||
/// anything here.
|
||||
@override
|
||||
void onTransitionEnd() {}
|
||||
|
||||
@override
|
||||
Coordinate calculateVelocity(Coordinate start_, Coordinate target,
|
||||
[num decelerationFactor = null]) {
|
||||
return new Coordinate(
|
||||
physicsX.solve(start_.x, target.x, decelerationFactor),
|
||||
[num decelerationFactor]) {
|
||||
return Coordinate(physicsX.solve(start_.x, target.x, decelerationFactor),
|
||||
physicsY.solve(start_.y, target.y, decelerationFactor));
|
||||
}
|
||||
|
||||
@override
|
||||
bool start(Coordinate velocity, Coordinate minCoord, Coordinate maxCoord,
|
||||
Coordinate initialOffset,
|
||||
[num decelerationFactor = null]) {
|
||||
[num decelerationFactor]) {
|
||||
_customDecelerationFactor = _defaultDecelerationFactor;
|
||||
if (decelerationFactor != null) {
|
||||
_customDecelerationFactor = decelerationFactor;
|
||||
|
@ -453,7 +409,7 @@ class TimeoutMomentum implements Momentum {
|
|||
_customDecelerationFactor, velocity.y);
|
||||
if (!physicsX.isDone() || !physicsY.isDone()) {
|
||||
_calculateMoves();
|
||||
if (!_moves.isEmpty) {
|
||||
if (_moves.isNotEmpty) {
|
||||
num firstTime = _moves.first.time;
|
||||
_stepTimeout = Env.requestAnimationFrame(_step, null, firstTime);
|
||||
_decelerating = true;
|
||||
|
@ -464,12 +420,10 @@ class TimeoutMomentum implements Momentum {
|
|||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the x, y values of the element offset without actually moving the
|
||||
* element. This is done because we store decimal values for x, y for
|
||||
* precision, but moving is only required when the offset is changed by at
|
||||
* least a whole integer.
|
||||
*/
|
||||
/// Update the x, y values of the element offset without actually moving the
|
||||
/// element. This is done because we store decimal values for x, y for
|
||||
/// precision, but moving is only required when the offset is changed by at
|
||||
/// least a whole integer.
|
||||
void _stepWithoutAnimation() {
|
||||
physicsX.step();
|
||||
physicsY.step();
|
||||
|
@ -477,25 +431,23 @@ class TimeoutMomentum implements Momentum {
|
|||
_nextY = physicsY._currentOffset.round();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the next offset of the element and animate it to that position.
|
||||
*/
|
||||
/// Calculate the next offset of the element and animate it to that position.
|
||||
void _step(num timestamp) {
|
||||
_stepTimeout = null;
|
||||
|
||||
// Prune moves that are more than 1 frame behind when we have more
|
||||
// available moves.
|
||||
num lastEpoch = timestamp - SingleDimensionPhysics._MS_PER_FRAME;
|
||||
while (!_moves.isEmpty &&
|
||||
while (_moves.isNotEmpty &&
|
||||
!identical(_moves.first, _moves.last) &&
|
||||
_moves.first.time < lastEpoch) {
|
||||
_moves.removeFirst();
|
||||
}
|
||||
|
||||
if (!_moves.isEmpty) {
|
||||
if (_moves.isNotEmpty) {
|
||||
final move = _moves.removeFirst();
|
||||
_delegate.onDecelerate(move.x, move.y);
|
||||
if (!_moves.isEmpty) {
|
||||
if (_moves.isNotEmpty) {
|
||||
num nextTime = _moves.first.time;
|
||||
assert(_stepTimeout == null);
|
||||
_stepTimeout = Env.requestAnimationFrame(_step, null, nextTime);
|
||||
|
@ -505,6 +457,7 @@ class TimeoutMomentum implements Momentum {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void abort() {
|
||||
_decelerating = false;
|
||||
_moves.clear();
|
||||
|
@ -514,20 +467,20 @@ class TimeoutMomentum implements Momentum {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Coordinate stop() {
|
||||
final wasDecelerating = _decelerating;
|
||||
_decelerating = false;
|
||||
Coordinate velocity;
|
||||
if (!_moves.isEmpty) {
|
||||
if (_moves.isNotEmpty) {
|
||||
final move = _moves.first;
|
||||
// This is a workaround for the ugly hacks that get applied when a user
|
||||
// passed a velocity in to this Momentum implementation.
|
||||
num velocityScale = SingleDimensionPhysics._MS_PER_FRAME *
|
||||
SingleDimensionPhysics._INITIAL_VELOCITY_BOOST_FACTOR;
|
||||
velocity =
|
||||
new Coordinate(move.vx / velocityScale, move.vy / velocityScale);
|
||||
velocity = Coordinate(move.vx / velocityScale, move.vy / velocityScale);
|
||||
} else {
|
||||
velocity = new Coordinate(0, 0);
|
||||
velocity = Coordinate(0, 0);
|
||||
}
|
||||
_moves.clear();
|
||||
if (_stepTimeout != null) {
|
||||
|
@ -540,10 +493,11 @@ class TimeoutMomentum implements Momentum {
|
|||
return velocity;
|
||||
}
|
||||
|
||||
@override
|
||||
Coordinate get destination {
|
||||
if (!_moves.isEmpty) {
|
||||
if (_moves.isNotEmpty) {
|
||||
final lastMove = _moves.last;
|
||||
return new Coordinate(lastMove.x, lastMove.y);
|
||||
return Coordinate(lastMove.x, lastMove.y);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -7,49 +7,41 @@
|
|||
part of touch;
|
||||
|
||||
abstract class ScrollListener {
|
||||
/**
|
||||
* The callback invoked for a scroll event.
|
||||
* [decelerating] specifies whether or not the content is moving due
|
||||
* to deceleration. It should be false if the content is moving because the
|
||||
* user is dragging the content.
|
||||
*/
|
||||
/// The callback invoked for a scroll event.
|
||||
/// [decelerating] specifies whether or not the content is moving due
|
||||
/// to deceleration. It should be false if the content is moving because the
|
||||
/// user is dragging the content.
|
||||
void onScrollerMoved(double scrollX, double scrollY, bool decelerating);
|
||||
}
|
||||
|
||||
/**
|
||||
* The scroll watcher is intended to provide a single way to
|
||||
* listen for scroll events from instances of Scroller.
|
||||
* TODO(jacobr): this class is obsolete.
|
||||
*/
|
||||
/// The scroll watcher is intended to provide a single way to
|
||||
/// listen for scroll events from instances of Scroller.
|
||||
/// TODO(jacobr): this class is obsolete.
|
||||
class ScrollWatcher {
|
||||
Scroller _scroller;
|
||||
final Scroller _scroller;
|
||||
|
||||
List<ScrollListener> _listeners;
|
||||
final List<ScrollListener> _listeners;
|
||||
|
||||
Element _scrollerEl;
|
||||
|
||||
ScrollWatcher(Scroller scroller)
|
||||
: _scroller = scroller,
|
||||
_listeners = new List<ScrollListener>() {}
|
||||
_listeners = <ScrollListener>[];
|
||||
|
||||
void addListener(ScrollListener listener) {
|
||||
_listeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the scroll event to all listeners.
|
||||
* [decelerating] is true if the offset is changing because of deceleration.
|
||||
*/
|
||||
/// Send the scroll event to all listeners.
|
||||
/// [decelerating] is true if the offset is changing because of deceleration.
|
||||
void _dispatchScroll(num scrollX, num scrollY, [bool decelerating = false]) {
|
||||
for (final listener in _listeners) {
|
||||
listener.onScrollerMoved(scrollX, scrollY, decelerating);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes elements and event handlers. Must be called after construction
|
||||
* and before usage.
|
||||
*/
|
||||
/// Initializes elements and event handlers. Must be called after construction
|
||||
/// and before usage.
|
||||
void initialize() {
|
||||
_scrollerEl = _scroller.getElement();
|
||||
_scroller.onContentMoved.listen((e) {
|
||||
|
@ -57,9 +49,7 @@ class ScrollWatcher {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This callback is invoked any time the scroller content offset changes.
|
||||
*/
|
||||
/// This callback is invoked any time the scroller content offset changes.
|
||||
void _onContentMoved(Event e) {
|
||||
num scrollX = _scroller.getHorizontalOffset();
|
||||
num scrollY = _scroller.getVerticalOffset();
|
||||
|
|
|
@ -6,45 +6,35 @@
|
|||
|
||||
part of touch;
|
||||
|
||||
/**
|
||||
* Implementation of a scrollbar for the custom scrolling behavior
|
||||
* defined in [:Scroller:].
|
||||
*/
|
||||
/// Implementation of a scrollbar for the custom scrolling behavior
|
||||
/// defined in [:Scroller:].
|
||||
class Scrollbar implements ScrollListener {
|
||||
/**
|
||||
* The minimum size of scrollbars when not compressed.
|
||||
*/
|
||||
/// The minimum size of scrollbars when not compressed.
|
||||
static const _MIN_SIZE = 30;
|
||||
|
||||
/**
|
||||
* The minimum compressed size of scrollbars. Scrollbars are compressed when
|
||||
* the content is stretching past its boundaries.
|
||||
*/
|
||||
/// The minimum compressed size of scrollbars. Scrollbars are compressed when
|
||||
/// the content is stretching past its boundaries.
|
||||
static const _MIN_COMPRESSED_SIZE = 8;
|
||||
/** Padding in pixels to add above and bellow the scrollbar. */
|
||||
|
||||
/// Padding in pixels to add above and bellow the scrollbar. */
|
||||
static const _PADDING_LENGTH = 10;
|
||||
/**
|
||||
* The amount of time to wait before hiding scrollbars after showing them.
|
||||
* Measured in ms.
|
||||
*/
|
||||
|
||||
/// The amount of time to wait before hiding scrollbars after showing them.
|
||||
/// Measured in ms.
|
||||
static const _DISPLAY_TIME = 300;
|
||||
static const DRAG_CLASS_NAME = 'drag';
|
||||
|
||||
Scroller _scroller;
|
||||
Element _frame;
|
||||
final Scroller _scroller;
|
||||
final Element _frame;
|
||||
bool _scrollInProgress = false;
|
||||
bool _scrollBarDragInProgressValue = false;
|
||||
|
||||
/**
|
||||
* Cached values of height and width. Keys will be 'height' and 'width'
|
||||
* depending on if they are applied to vertical or horizontal scrollbar.
|
||||
*/
|
||||
Map<String, num> _cachedSize;
|
||||
/// Cached values of height and width. Keys will be 'height' and 'width'
|
||||
/// depending on if they are applied to vertical or horizontal scrollbar.
|
||||
final Map<String, num> _cachedSize;
|
||||
|
||||
/**
|
||||
* This bound function will be used as the input to window.setTimeout when
|
||||
* scheduling the hiding of the scrollbars.
|
||||
*/
|
||||
/// This bound function will be used as the input to window.setTimeout when
|
||||
/// scheduling the hiding of the scrollbars.
|
||||
Function _boundHideFn;
|
||||
|
||||
Element _verticalElement;
|
||||
|
@ -56,14 +46,14 @@ class Scrollbar implements ScrollListener {
|
|||
num _currentScrollRatio;
|
||||
Timer _timer;
|
||||
|
||||
bool _displayOnHover;
|
||||
final bool _displayOnHover;
|
||||
bool _hovering = false;
|
||||
|
||||
Scrollbar(Scroller scroller, [displayOnHover = true])
|
||||
: _displayOnHover = displayOnHover,
|
||||
_scroller = scroller,
|
||||
_frame = scroller.getFrame(),
|
||||
_cachedSize = new Map<String, num>() {
|
||||
_cachedSize = <String, num>{} {
|
||||
_boundHideFn = () {
|
||||
_showScrollbars(false);
|
||||
};
|
||||
|
@ -71,7 +61,7 @@ class Scrollbar implements ScrollListener {
|
|||
|
||||
bool get _scrollBarDragInProgress => _scrollBarDragInProgressValue;
|
||||
|
||||
void set _scrollBarDragInProgress(bool value) {
|
||||
set _scrollBarDragInProgress(bool value) {
|
||||
_scrollBarDragInProgressValue = value;
|
||||
_toggleClass(
|
||||
_verticalElement, DRAG_CLASS_NAME, value && _currentScrollVertical);
|
||||
|
@ -90,10 +80,8 @@ class Scrollbar implements ScrollListener {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes elements and event handlers. Must be called after
|
||||
* construction and before usage.
|
||||
*/
|
||||
/// Initializes elements and event handlers. Must be called after
|
||||
/// construction and before usage.
|
||||
void initialize() {
|
||||
// Don't initialize if we have already been initialized.
|
||||
// TODO(jacobr): remove this once bugs are fixed and enterDocument is only
|
||||
|
@ -101,9 +89,9 @@ class Scrollbar implements ScrollListener {
|
|||
if (_verticalElement != null) {
|
||||
return;
|
||||
}
|
||||
_verticalElement = new Element.html(
|
||||
_verticalElement = Element.html(
|
||||
'<div class="touch-scrollbar touch-scrollbar-vertical"></div>');
|
||||
_horizontalElement = new Element.html(
|
||||
_horizontalElement = Element.html(
|
||||
'<div class="touch-scrollbar touch-scrollbar-horizontal"></div>');
|
||||
_scroller.addScrollListener(this);
|
||||
|
||||
|
@ -223,19 +211,17 @@ class Scrollbar implements ScrollListener {
|
|||
void _onEnd(Event e) {
|
||||
_scrollBarDragInProgress = false;
|
||||
// TODO(jacobr): make scrollbar less tightly coupled to the scroller.
|
||||
_scroller._onScrollerDragEnd.add(new Event(ScrollerEventType.DRAG_END));
|
||||
_scroller._onScrollerDragEnd.add(Event(ScrollerEventType.DRAG_END));
|
||||
}
|
||||
|
||||
/**
|
||||
* When scrolling ends, schedule a timeout to hide the scrollbars.
|
||||
*/
|
||||
/// When scrolling ends, schedule a timeout to hide the scrollbars.
|
||||
void _onScrollerEnd(Event e) {
|
||||
_cancelTimeout();
|
||||
_timer =
|
||||
new Timer(const Duration(milliseconds: _DISPLAY_TIME), _boundHideFn);
|
||||
_timer = Timer(const Duration(milliseconds: _DISPLAY_TIME), _boundHideFn);
|
||||
_scrollInProgress = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void onScrollerMoved(num scrollX, num scrollY, bool decelerating) {
|
||||
if (_scrollInProgress == false) {
|
||||
// Display the scrollbar and then immediately prepare to hide it...
|
||||
|
@ -270,9 +256,7 @@ class Scrollbar implements ScrollListener {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When scrolling starts, show scrollbars and clear hide intervals.
|
||||
*/
|
||||
/// When scrolling starts, show scrollbars and clear hide intervals.
|
||||
void _onScrollerStart(Event e) {
|
||||
_scrollInProgress = true;
|
||||
_cancelTimeout();
|
||||
|
@ -286,9 +270,7 @@ class Scrollbar implements ScrollListener {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show or hide the scrollbars by changing the opacity.
|
||||
*/
|
||||
/// Show or hide the scrollbars by changing the opacity.
|
||||
void _showScrollbars(bool show) {
|
||||
if (_hovering == true && _displayOnHover) {
|
||||
show = true;
|
||||
|
@ -312,12 +294,10 @@ class Scrollbar implements ScrollListener {
|
|||
frameSize - _PADDING_LENGTH * 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the vertical or horizontal scrollbar based on the new scroll
|
||||
* properties. The CSS property to adjust for position (bottom|right) is
|
||||
* specified by [cssPos]. The CSS property to adjust for size (height|width)
|
||||
* is specified by [cssSize].
|
||||
*/
|
||||
/// Update the vertical or horizontal scrollbar based on the new scroll
|
||||
/// properties. The CSS property to adjust for position (bottom|right) is
|
||||
/// specified by [cssPos]. The CSS property to adjust for size (height|width)
|
||||
/// is specified by [cssSize].
|
||||
void _updateScrollbar(Element element, num offset, num scrollPercent,
|
||||
num frameSize, num contentSize, String cssPos, String cssSize) {
|
||||
if (!_cachedSize.containsKey(cssSize)) {
|
||||
|
|
|
@ -6,35 +6,33 @@
|
|||
|
||||
part of touch;
|
||||
|
||||
/**
|
||||
* Implementation of a custom scrolling behavior.
|
||||
* This behavior overrides native scrolling for an area. This area can be a
|
||||
* single defined part of a page, the entire page, or several different parts
|
||||
* of a page.
|
||||
*
|
||||
* To use this scrolling behavior you need to define a frame and the content.
|
||||
* The frame defines the area that the content will scroll within. The frame and
|
||||
* content must both be HTML Elements, with the content being a direct child of
|
||||
* the frame. Usually the frame is smaller in size than the content. This is
|
||||
* not necessary though, if the content is smaller then bouncing will occur to
|
||||
* provide feedback that you are past the scrollable area.
|
||||
*
|
||||
* The scrolling behavior works using the webkit translate3d transformation,
|
||||
* which means browsers that do not have hardware accelerated transformations
|
||||
* will not perform as well using this. Simple scrolling should be fine even
|
||||
* without hardware acceleration, but animating momentum and deceleration is
|
||||
* unacceptably slow without it. There is also the option to use relative
|
||||
* positioning (setting the left and top styles).
|
||||
*
|
||||
* For this to work properly you need to set -webkit-text-size-adjust to 'none'
|
||||
* on an ancestor element of the frame, or on the frame itself. If you forget
|
||||
* this you may see the text content of the scrollable area changing size as it
|
||||
* moves.
|
||||
*
|
||||
* The behavior is intended to support vertical and horizontal scrolling, and
|
||||
* scrolling with momentum when a touch gesture flicks with enough velocity.
|
||||
*/
|
||||
typedef void Callback();
|
||||
/// Implementation of a custom scrolling behavior.
|
||||
/// This behavior overrides native scrolling for an area. This area can be a
|
||||
/// single defined part of a page, the entire page, or several different parts
|
||||
/// of a page.
|
||||
///
|
||||
/// To use this scrolling behavior you need to define a frame and the content.
|
||||
/// The frame defines the area that the content will scroll within. The frame and
|
||||
/// content must both be HTML Elements, with the content being a direct child of
|
||||
/// the frame. Usually the frame is smaller in size than the content. This is
|
||||
/// not necessary though, if the content is smaller then bouncing will occur to
|
||||
/// provide feedback that you are past the scrollable area.
|
||||
///
|
||||
/// The scrolling behavior works using the webkit translate3d transformation,
|
||||
/// which means browsers that do not have hardware accelerated transformations
|
||||
/// will not perform as well using this. Simple scrolling should be fine even
|
||||
/// without hardware acceleration, but animating momentum and deceleration is
|
||||
/// unacceptably slow without it. There is also the option to use relative
|
||||
/// positioning (setting the left and top styles).
|
||||
///
|
||||
/// For this to work properly you need to set -webkit-text-size-adjust to 'none'
|
||||
/// on an ancestor element of the frame, or on the frame itself. If you forget
|
||||
/// this you may see the text content of the scrollable area changing size as it
|
||||
/// moves.
|
||||
///
|
||||
/// The behavior is intended to support vertical and horizontal scrolling, and
|
||||
/// scrolling with momentum when a touch gesture flicks with enough velocity.
|
||||
typedef Callback = void Function();
|
||||
|
||||
// Helper method to await the completion of 2 futures.
|
||||
void joinFutures(List<Future> futures, Callback callback) {
|
||||
|
@ -53,7 +51,7 @@ void joinFutures(List<Future> futures, Callback callback) {
|
|||
}
|
||||
|
||||
class Scroller implements Draggable, MomentumDelegate {
|
||||
/** Pixels to move each time an arrow key is pressed. */
|
||||
/// Pixels to move each time an arrow key is pressed. */
|
||||
static const ARROW_KEY_DELTA = 30;
|
||||
static const SCROLL_WHEEL_VELOCITY = 0.01;
|
||||
static const FAST_SNAP_DECELERATION_FACTOR = 0.84;
|
||||
|
@ -62,16 +60,14 @@ class Scroller implements Draggable, MomentumDelegate {
|
|||
// TODO(jacobr): remove this static variable.
|
||||
static bool _dragInProgress = false;
|
||||
|
||||
/** The node that will actually scroll. */
|
||||
Element _element;
|
||||
/// The node that will actually scroll. */
|
||||
final Element _element;
|
||||
|
||||
/**
|
||||
* Frame is the node that will serve as the container for the scrolling
|
||||
* content.
|
||||
*/
|
||||
Element _frame;
|
||||
/// Frame is the node that will serve as the container for the scrolling
|
||||
/// content.
|
||||
final Element _frame;
|
||||
|
||||
/** Touch manager to track the events on the scrollable area. */
|
||||
/// Touch manager to track the events on the scrollable area. */
|
||||
TouchHandler _touchHandler;
|
||||
|
||||
Momentum _momentum;
|
||||
|
@ -87,57 +83,48 @@ class Scroller implements Draggable, MomentumDelegate {
|
|||
StreamController<Event> _onDecelStart;
|
||||
Stream<Event> _onDecelStartStream;
|
||||
|
||||
/** Set if vertical scrolling should be enabled. */
|
||||
/// Set if vertical scrolling should be enabled. */
|
||||
@override
|
||||
bool verticalEnabled;
|
||||
|
||||
/** Set if horizontal scrolling should be enabled. */
|
||||
/// Set if horizontal scrolling should be enabled. */
|
||||
@override
|
||||
bool horizontalEnabled;
|
||||
|
||||
/**
|
||||
* Set if momentum should be enabled.
|
||||
*/
|
||||
/// Set if momentum should be enabled.
|
||||
bool _momentumEnabled;
|
||||
|
||||
/** Set which type of scrolling translation technique should be used. */
|
||||
int _scrollTechnique;
|
||||
/// Set which type of scrolling translation technique should be used. */
|
||||
final int _scrollTechnique;
|
||||
|
||||
/**
|
||||
* The maximum coordinate that the left upper corner of the content can scroll
|
||||
* to.
|
||||
*/
|
||||
/// The maximum coordinate that the left upper corner of the content can scroll
|
||||
/// to.
|
||||
Coordinate _maxPoint;
|
||||
|
||||
/**
|
||||
* An offset to subtract from the maximum coordinate that the left upper
|
||||
* corner of the content can scroll to.
|
||||
*/
|
||||
Coordinate _maxOffset;
|
||||
/// An offset to subtract from the maximum coordinate that the left upper
|
||||
/// corner of the content can scroll to.
|
||||
final Coordinate _maxOffset;
|
||||
|
||||
/**
|
||||
* An offset to add to the minimum coordinate that the left upper corner of
|
||||
* the content can scroll to.
|
||||
*/
|
||||
Coordinate _minOffset;
|
||||
/// An offset to add to the minimum coordinate that the left upper corner of
|
||||
/// the content can scroll to.
|
||||
final Coordinate _minOffset;
|
||||
|
||||
/** Initialize the current content offset. */
|
||||
Coordinate _contentOffset;
|
||||
/// Initialize the current content offset. */
|
||||
final Coordinate _contentOffset;
|
||||
|
||||
// TODO(jacobr): the function type is
|
||||
// [:Function(Element, num, num)->void:].
|
||||
/**
|
||||
* The function to use that will actually translate the scrollable node.
|
||||
*/
|
||||
/// The function to use that will actually translate the scrollable node.
|
||||
Function _setOffsetFunction;
|
||||
/**
|
||||
* Function that returns the content size that can be specified instead of
|
||||
* querying the DOM.
|
||||
*/
|
||||
Function _lookupContentSizeDelegate;
|
||||
|
||||
/// Function that returns the content size that can be specified instead of
|
||||
/// querying the DOM.
|
||||
final Function _lookupContentSizeDelegate;
|
||||
|
||||
Size _scrollSize;
|
||||
Size _contentSize;
|
||||
Coordinate _minPoint;
|
||||
bool _isStopping = false;
|
||||
final bool _isStopping = false;
|
||||
Coordinate _contentStartOffset;
|
||||
bool _started = false;
|
||||
bool _activeGesture = false;
|
||||
|
@ -147,24 +134,23 @@ class Scroller implements Draggable, MomentumDelegate {
|
|||
[this.verticalEnabled = false,
|
||||
this.horizontalEnabled = false,
|
||||
momentumEnabled = true,
|
||||
lookupContentSizeDelegate = null,
|
||||
lookupContentSizeDelegate,
|
||||
num defaultDecelerationFactor = 1,
|
||||
int scrollTechnique = null,
|
||||
int scrollTechnique,
|
||||
bool capture = false])
|
||||
: _momentumEnabled = momentumEnabled,
|
||||
_lookupContentSizeDelegate = lookupContentSizeDelegate,
|
||||
_element = scrollableElem,
|
||||
_frame = scrollableElem.parent,
|
||||
_scrollTechnique = scrollTechnique != null
|
||||
? scrollTechnique
|
||||
: ScrollerScrollTechnique.TRANSFORM_3D,
|
||||
_minPoint = new Coordinate(0, 0),
|
||||
_maxPoint = new Coordinate(0, 0),
|
||||
_maxOffset = new Coordinate(0, 0),
|
||||
_minOffset = new Coordinate(0, 0),
|
||||
_contentOffset = new Coordinate(0, 0) {
|
||||
_touchHandler = new TouchHandler(this, scrollableElem.parent);
|
||||
_momentum = new Momentum(this, defaultDecelerationFactor);
|
||||
_scrollTechnique =
|
||||
scrollTechnique ?? ScrollerScrollTechnique.TRANSFORM_3D,
|
||||
_minPoint = Coordinate(0, 0),
|
||||
_maxPoint = Coordinate(0, 0),
|
||||
_maxOffset = Coordinate(0, 0),
|
||||
_minOffset = Coordinate(0, 0),
|
||||
_contentOffset = Coordinate(0, 0) {
|
||||
_touchHandler = TouchHandler(this, scrollableElem.parent);
|
||||
_momentum = Momentum(this, defaultDecelerationFactor);
|
||||
|
||||
Element parentElem = scrollableElem.parent;
|
||||
assert(parentElem != null);
|
||||
|
@ -246,7 +232,7 @@ class Scroller implements Draggable, MomentumDelegate {
|
|||
|
||||
Stream<Event> get onScrollerStart {
|
||||
if (_onScrollerStart == null) {
|
||||
_onScrollerStart = new StreamController<Event>.broadcast(sync: true);
|
||||
_onScrollerStart = StreamController<Event>.broadcast(sync: true);
|
||||
_onScrollerStartStream = _onScrollerStart.stream;
|
||||
}
|
||||
return _onScrollerStartStream;
|
||||
|
@ -254,7 +240,7 @@ class Scroller implements Draggable, MomentumDelegate {
|
|||
|
||||
Stream<Event> get onScrollerEnd {
|
||||
if (_onScrollerEnd == null) {
|
||||
_onScrollerEnd = new StreamController<Event>.broadcast(sync: true);
|
||||
_onScrollerEnd = StreamController<Event>.broadcast(sync: true);
|
||||
_onScrollerEndStream = _onScrollerEnd.stream;
|
||||
}
|
||||
return _onScrollerEndStream;
|
||||
|
@ -262,7 +248,7 @@ class Scroller implements Draggable, MomentumDelegate {
|
|||
|
||||
Stream<Event> get onScrollerDragEnd {
|
||||
if (_onScrollerDragEnd == null) {
|
||||
_onScrollerDragEnd = new StreamController<Event>.broadcast(sync: true);
|
||||
_onScrollerDragEnd = StreamController<Event>.broadcast(sync: true);
|
||||
_onScrollerDragEndStream = _onScrollerDragEnd.stream;
|
||||
}
|
||||
return _onScrollerDragEndStream;
|
||||
|
@ -270,7 +256,7 @@ class Scroller implements Draggable, MomentumDelegate {
|
|||
|
||||
Stream<Event> get onContentMoved {
|
||||
if (_onContentMoved == null) {
|
||||
_onContentMoved = new StreamController<Event>.broadcast(sync: true);
|
||||
_onContentMoved = StreamController<Event>.broadcast(sync: true);
|
||||
_onContentMovedStream = _onContentMoved.stream;
|
||||
}
|
||||
return _onContentMovedStream;
|
||||
|
@ -278,28 +264,24 @@ class Scroller implements Draggable, MomentumDelegate {
|
|||
|
||||
Stream<Event> get onDecelStart {
|
||||
if (_onDecelStart == null) {
|
||||
_onDecelStart = new StreamController<Event>.broadcast(sync: true);
|
||||
_onDecelStart = StreamController<Event>.broadcast(sync: true);
|
||||
_onDecelStartStream = _onDecelStart.stream;
|
||||
}
|
||||
return _onDecelStartStream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a scroll listener. This allows other classes to subscribe to scroll
|
||||
* notifications from this scroller.
|
||||
*/
|
||||
/// Add a scroll listener. This allows other classes to subscribe to scroll
|
||||
/// notifications from this scroller.
|
||||
void addScrollListener(ScrollListener listener) {
|
||||
if (_scrollWatcher == null) {
|
||||
_scrollWatcher = new ScrollWatcher(this);
|
||||
_scrollWatcher = ScrollWatcher(this);
|
||||
_scrollWatcher.initialize();
|
||||
}
|
||||
_scrollWatcher.addListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust the new calculated scroll position based on the minimum allowed
|
||||
* position and returns the adjusted scroll value.
|
||||
*/
|
||||
/// Adjust the new calculated scroll position based on the minimum allowed
|
||||
/// position and returns the adjusted scroll value.
|
||||
num _adjustValue(num newPosition, num minPosition, num maxPosition) {
|
||||
assert(minPosition <= maxPosition);
|
||||
|
||||
|
@ -313,32 +295,24 @@ class Scroller implements Draggable, MomentumDelegate {
|
|||
return newPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Coordinate we would end up at if we did nothing.
|
||||
*/
|
||||
/// Coordinate we would end up at if we did nothing.
|
||||
Coordinate get currentTarget {
|
||||
Coordinate end = _momentum.destination;
|
||||
if (end == null) {
|
||||
end = _contentOffset;
|
||||
}
|
||||
end ??= _contentOffset;
|
||||
return end;
|
||||
}
|
||||
|
||||
Coordinate get contentOffset => _contentOffset;
|
||||
|
||||
/**
|
||||
* Animate the position of the scroller to the specified [x], [y] coordinates
|
||||
* by applying the throw gesture with the correct velocity to end at that
|
||||
* location.
|
||||
*/
|
||||
void throwTo(num x, num y, [num decelerationFactor = null]) {
|
||||
/// Animate the position of the scroller to the specified [x], [y] coordinates
|
||||
/// by applying the throw gesture with the correct velocity to end at that
|
||||
/// location.
|
||||
void throwTo(num x, num y, [num decelerationFactor]) {
|
||||
reconfigure(() {
|
||||
final snappedTarget = _snapToBounds(x, y);
|
||||
// If a deceleration factor is not specified, use the existing
|
||||
// deceleration factor specified by the momentum simulator.
|
||||
if (decelerationFactor == null) {
|
||||
decelerationFactor = _momentum.decelerationFactor;
|
||||
}
|
||||
decelerationFactor ??= _momentum.decelerationFactor;
|
||||
|
||||
if (snappedTarget != currentTarget) {
|
||||
_momentum.abort();
|
||||
|
@ -348,13 +322,13 @@ class Scroller implements Draggable, MomentumDelegate {
|
|||
_contentOffset, snappedTarget, decelerationFactor),
|
||||
decelerationFactor);
|
||||
if (_onDecelStart != null) {
|
||||
_onDecelStart.add(new Event(ScrollerEventType.DECEL_START));
|
||||
_onDecelStart.add(Event(ScrollerEventType.DECEL_START));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void throwDelta(num deltaX, num deltaY, [num decelerationFactor = null]) {
|
||||
void throwDelta(num deltaX, num deltaY, [num decelerationFactor]) {
|
||||
Coordinate start = _contentOffset;
|
||||
Coordinate end = currentTarget;
|
||||
int x = end.x.toInt();
|
||||
|
@ -380,28 +354,25 @@ class Scroller implements Draggable, MomentumDelegate {
|
|||
_setContentOffset(_contentOffset.x, _contentOffset.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusted content size is a size with the combined largest height and width
|
||||
* of both the content and the frame.
|
||||
*/
|
||||
/// Adjusted content size is a size with the combined largest height and width
|
||||
/// of both the content and the frame.
|
||||
Size _getAdjustedContentSize() {
|
||||
return new Size(Math.max(_scrollSize.width, _contentSize.width),
|
||||
return Size(Math.max(_scrollSize.width, _contentSize.width),
|
||||
Math.max(_scrollSize.height, _contentSize.height));
|
||||
}
|
||||
|
||||
// TODO(jmesserly): these should be properties instead of get* methods
|
||||
num getDefaultVerticalOffset() => _maxPoint.y;
|
||||
@override
|
||||
Element getElement() => _element;
|
||||
Element getFrame() => _frame;
|
||||
num getHorizontalOffset() => _contentOffset.x;
|
||||
|
||||
/**
|
||||
* [x] Value to use as reference for percent measurement. If
|
||||
* none is provided then the content's current x offset will be used.
|
||||
* Returns the percent of the page scrolled horizontally.
|
||||
*/
|
||||
num getHorizontalScrollPercent([num x = null]) {
|
||||
x = x != null ? x : _contentOffset.x;
|
||||
/// [x] Value to use as reference for percent measurement. If
|
||||
/// none is provided then the content's current x offset will be used.
|
||||
/// Returns the percent of the page scrolled horizontally.
|
||||
num getHorizontalScrollPercent([num x]) {
|
||||
x = x ?? _contentOffset.x;
|
||||
return (x - _minPoint.x) / (_maxPoint.x - _minPoint.x);
|
||||
}
|
||||
|
||||
|
@ -409,25 +380,19 @@ class Scroller implements Draggable, MomentumDelegate {
|
|||
num getMinPointY() => _minPoint.y;
|
||||
Momentum get momentum => _momentum;
|
||||
|
||||
/**
|
||||
* Provide access to the touch handler that the scroller created to manage
|
||||
* touch events.
|
||||
*/
|
||||
/// Provide access to the touch handler that the scroller created to manage
|
||||
/// touch events.
|
||||
TouchHandler getTouchHandler() => _touchHandler;
|
||||
num getVerticalOffset() => _contentOffset.y;
|
||||
|
||||
/**
|
||||
* [y] value is used as reference for percent measurement. If
|
||||
* none is provided then the content's current y offset will be used.
|
||||
*/
|
||||
num getVerticalScrollPercent([num y = null]) {
|
||||
y = y != null ? y : _contentOffset.y;
|
||||
/// [y] value is used as reference for percent measurement. If
|
||||
/// none is provided then the content's current y offset will be used.
|
||||
num getVerticalScrollPercent([num y]) {
|
||||
y = y ?? _contentOffset.y;
|
||||
return (y - _minPoint.y) / Math.max(1, _maxPoint.y - _minPoint.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the dom elements necessary for the scrolling to work.
|
||||
*/
|
||||
/// Initialize the dom elements necessary for the scrolling to work.
|
||||
void _initLayer() {
|
||||
// The scrollable node provided to Scroller must be a direct child
|
||||
// of the scrollable frame.
|
||||
|
@ -436,17 +401,20 @@ class Scroller implements Draggable, MomentumDelegate {
|
|||
_setContentOffset(_maxPoint.x, _maxPoint.y);
|
||||
}
|
||||
|
||||
@override
|
||||
void onDecelerate(num x, num y) {
|
||||
_setContentOffset(x, y);
|
||||
}
|
||||
|
||||
@override
|
||||
void onDecelerationEnd() {
|
||||
if (_onScrollerEnd != null) {
|
||||
_onScrollerEnd.add(new Event(ScrollerEventType.SCROLLER_END));
|
||||
_onScrollerEnd.add(Event(ScrollerEventType.SCROLLER_END));
|
||||
}
|
||||
_started = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void onDragEnd() {
|
||||
_dragInProgress = false;
|
||||
|
||||
|
@ -458,23 +426,24 @@ class Scroller implements Draggable, MomentumDelegate {
|
|||
}
|
||||
|
||||
if (_onScrollerDragEnd != null) {
|
||||
_onScrollerDragEnd.add(new Event(ScrollerEventType.DRAG_END));
|
||||
_onScrollerDragEnd.add(Event(ScrollerEventType.DRAG_END));
|
||||
}
|
||||
|
||||
if (!decelerating) {
|
||||
_snapContentOffsetToBounds();
|
||||
if (_onScrollerEnd != null) {
|
||||
_onScrollerEnd.add(new Event(ScrollerEventType.SCROLLER_END));
|
||||
_onScrollerEnd.add(Event(ScrollerEventType.SCROLLER_END));
|
||||
}
|
||||
_started = false;
|
||||
} else {
|
||||
if (_onDecelStart != null) {
|
||||
_onDecelStart.add(new Event(ScrollerEventType.DECEL_START));
|
||||
_onDecelStart.add(Event(ScrollerEventType.DECEL_START));
|
||||
}
|
||||
}
|
||||
_activeGesture = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void onDragMove() {
|
||||
if (_isStopping || (!_activeGesture && _dragInProgress)) {
|
||||
return;
|
||||
|
@ -497,12 +466,13 @@ class Scroller implements Draggable, MomentumDelegate {
|
|||
if (!_started) {
|
||||
_started = true;
|
||||
if (_onScrollerStart != null) {
|
||||
_onScrollerStart.add(new Event(ScrollerEventType.SCROLLER_START));
|
||||
_onScrollerStart.add(Event(ScrollerEventType.SCROLLER_START));
|
||||
}
|
||||
}
|
||||
_setContentOffset(newX, newY);
|
||||
}
|
||||
|
||||
@override
|
||||
bool onDragStart(TouchEvent e) {
|
||||
if (e.touches.length > 1) {
|
||||
return false;
|
||||
|
@ -514,11 +484,11 @@ class Scroller implements Draggable, MomentumDelegate {
|
|||
return !!(shouldVertical || shouldHorizontal && !verticalish);
|
||||
}
|
||||
|
||||
@override
|
||||
void onTouchEnd() {}
|
||||
|
||||
/**
|
||||
* Prepare the scrollable area for possible movement.
|
||||
*/
|
||||
/// Prepare the scrollable area for possible movement.
|
||||
@override
|
||||
bool onTouchStart(TouchEvent e) {
|
||||
reconfigure(() {
|
||||
final touch = e.touches[0];
|
||||
|
@ -533,12 +503,10 @@ class Scroller implements Draggable, MomentumDelegate {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate dimensions of the frame and the content. Adjust the minPoint
|
||||
* and maxPoint allowed for scrolling and scroll to a valid position. Call
|
||||
* this method if you know the frame or content has been updated. Called
|
||||
* internally on every touchstart event the frame receives.
|
||||
*/
|
||||
/// Recalculate dimensions of the frame and the content. Adjust the minPoint
|
||||
/// and maxPoint allowed for scrolling and scroll to a valid position. Call
|
||||
/// this method if you know the frame or content has been updated. Called
|
||||
/// internally on every touchstart event the frame receives.
|
||||
void reconfigure(Callback callback) {
|
||||
_resize(() {
|
||||
_snapContentOffsetToBounds();
|
||||
|
@ -556,22 +524,20 @@ class Scroller implements Draggable, MomentumDelegate {
|
|||
reconfigure(() => _setContentOffset(_maxPoint.x, _maxPoint.y));
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculate dimensions of the frame and the content. Adjust the minPoint
|
||||
* and maxPoint allowed for scrolling.
|
||||
*/
|
||||
/// Recalculate dimensions of the frame and the content. Adjust the minPoint
|
||||
/// and maxPoint allowed for scrolling.
|
||||
void _resize(Callback callback) {
|
||||
scheduleMicrotask(() {
|
||||
if (_lookupContentSizeDelegate != null) {
|
||||
_contentSize = _lookupContentSizeDelegate();
|
||||
} else {
|
||||
_contentSize = new Size(_element.scrollWidth, _element.scrollHeight);
|
||||
_contentSize = Size(_element.scrollWidth, _element.scrollHeight);
|
||||
}
|
||||
|
||||
_scrollSize = new Size(_frame.offset.width, _frame.offset.height);
|
||||
_scrollSize = Size(_frame.offset.width, _frame.offset.height);
|
||||
Size adjusted = _getAdjustedContentSize();
|
||||
_maxPoint = new Coordinate(-_maxOffset.x, -_maxOffset.y);
|
||||
_minPoint = new Coordinate(
|
||||
_maxPoint = Coordinate(-_maxOffset.x, -_maxOffset.y);
|
||||
_minPoint = Coordinate(
|
||||
Math.min(
|
||||
_scrollSize.width - adjusted.width + _minOffset.x, _maxPoint.x),
|
||||
Math.min(_scrollSize.height - adjusted.height + _minOffset.y,
|
||||
|
@ -583,61 +549,49 @@ class Scroller implements Draggable, MomentumDelegate {
|
|||
Coordinate _snapToBounds(num x, num y) {
|
||||
num clampX = GoogleMath.clamp(_minPoint.x, x, _maxPoint.x);
|
||||
num clampY = GoogleMath.clamp(_minPoint.y, y, _maxPoint.y);
|
||||
return new Coordinate(clampX, clampY);
|
||||
return Coordinate(clampX, clampY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Translate the content to a new position specified in px.
|
||||
*/
|
||||
/// Translate the content to a new position specified in px.
|
||||
void _setContentOffset(num x, num y) {
|
||||
_contentOffset.x = x;
|
||||
_contentOffset.y = y;
|
||||
_setOffsetFunction(_element, x, y);
|
||||
if (_onContentMoved != null) {
|
||||
_onContentMoved.add(new Event(ScrollerEventType.CONTENT_MOVED));
|
||||
_onContentMoved.add(Event(ScrollerEventType.CONTENT_MOVED));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable or disable momentum.
|
||||
*/
|
||||
/// Enable or disable momentum.
|
||||
void setMomentum(bool enable) {
|
||||
_momentumEnabled = enable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the vertical scrolled offset of the element where [y] is the amount
|
||||
* of vertical space to be scrolled, in pixels.
|
||||
*/
|
||||
/// Sets the vertical scrolled offset of the element where [y] is the amount
|
||||
/// of vertical space to be scrolled, in pixels.
|
||||
void setVerticalOffset(num y) {
|
||||
_setContentOffset(_contentOffset.x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the scrollable area should scroll horizontally. Only
|
||||
* returns true if the client has enabled horizontal scrolling, and the
|
||||
* content is wider than the frame.
|
||||
*/
|
||||
/// Whether the scrollable area should scroll horizontally. Only
|
||||
/// returns true if the client has enabled horizontal scrolling, and the
|
||||
/// content is wider than the frame.
|
||||
bool _shouldScrollHorizontally() {
|
||||
return horizontalEnabled && _scrollSize.width < _contentSize.width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the scrollable area should scroll vertically. Only
|
||||
* returns true if the client has enabled vertical scrolling.
|
||||
* Vertical bouncing will occur even if frame is taller than content, because
|
||||
* this is what iPhone web apps tend to do. If this is not the desired
|
||||
* behavior, either disable vertical scrolling for this scroller or add a
|
||||
* 'bouncing' parameter to this interface.
|
||||
*/
|
||||
/// Whether the scrollable area should scroll vertically. Only
|
||||
/// returns true if the client has enabled vertical scrolling.
|
||||
/// Vertical bouncing will occur even if frame is taller than content, because
|
||||
/// this is what iPhone web apps tend to do. If this is not the desired
|
||||
/// behavior, either disable vertical scrolling for this scroller or add a
|
||||
/// 'bouncing' parameter to this interface.
|
||||
bool _shouldScrollVertically() {
|
||||
return verticalEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* In the event that the content is currently beyond the bounds of
|
||||
* the frame, snap it back in to place.
|
||||
*/
|
||||
/// In the event that the content is currently beyond the bounds of
|
||||
/// the frame, snap it back in to place.
|
||||
void _snapContentOffsetToBounds() {
|
||||
num clampX = GoogleMath.clamp(_minPoint.x, _contentOffset.x, _maxPoint.x);
|
||||
num clampY = GoogleMath.clamp(_minPoint.y, _contentOffset.y, _maxPoint.y);
|
||||
|
@ -646,12 +600,9 @@ class Scroller implements Draggable, MomentumDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate the deceleration behavior given a flick [velocity].
|
||||
* Returns true if deceleration has been initiated.
|
||||
*/
|
||||
bool _startDeceleration(Coordinate velocity,
|
||||
[num decelerationFactor = null]) {
|
||||
/// Initiate the deceleration behavior given a flick [velocity].
|
||||
/// Returns true if deceleration has been initiated.
|
||||
bool _startDeceleration(Coordinate velocity, [num decelerationFactor]) {
|
||||
if (!_shouldScrollHorizontally()) {
|
||||
velocity.x = 0;
|
||||
}
|
||||
|
@ -668,9 +619,7 @@ class Scroller implements Draggable, MomentumDelegate {
|
|||
return _momentum.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the deceleration of the scrollable content given a new position in px.
|
||||
*/
|
||||
/// Stop the deceleration of the scrollable content given a new position in px.
|
||||
void _stopDecelerating(num x, num y) {
|
||||
_momentum.stop();
|
||||
_setContentOffset(x, y);
|
||||
|
|
|
@ -6,13 +6,11 @@
|
|||
|
||||
part of touch;
|
||||
|
||||
/**
|
||||
* Convenience methods for dealing with time.
|
||||
* In the future this could also provide an entry point to mock out time
|
||||
* calculation for tests.
|
||||
*/
|
||||
/// Convenience methods for dealing with time.
|
||||
/// In the future this could also provide an entry point to mock out time
|
||||
/// calculation for tests.
|
||||
class TimeUtil {
|
||||
static int now() {
|
||||
return new DateTime.now().millisecondsSinceEpoch;
|
||||
return DateTime.now().millisecondsSinceEpoch;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,98 +6,84 @@
|
|||
|
||||
part of touch;
|
||||
|
||||
/**
|
||||
* Touch Handler. Class that handles all touch events and
|
||||
* uses them to interpret higher level gestures and behaviors. TouchEvent is a
|
||||
* built in mobile safari type:
|
||||
* [http://developer.apple.com/safari/library/documentation/UserExperience/Reference/TouchEventClassReference/TouchEvent/TouchEvent.html].
|
||||
*
|
||||
* Examples of higher level gestures this class is intended to support
|
||||
* - click, double click, long click
|
||||
* - dragging, swiping, zooming
|
||||
*
|
||||
* Touch Behavior:
|
||||
* Use this class to make your elements 'touchable' (see Touchable.dart).
|
||||
* Intended to work with all webkit browsers.
|
||||
*
|
||||
* Drag Behavior:
|
||||
* Use this class to make your elements 'draggable' (see draggable.js).
|
||||
* This behavior will handle all of the required events and report the
|
||||
* properties of the drag to you while the touch is happening and at the
|
||||
* end of the drag sequence. This behavior will NOT perform the actual
|
||||
* dragging (redrawing the element) for you, this responsibility is left to
|
||||
* the client code. This behavior contains a work around for a mobile
|
||||
* safari bug where the 'touchend' event is not dispatched when the touch
|
||||
* goes past the bottom of the browser window.
|
||||
* This is intended to work well in iframes.
|
||||
* Intended to work with all webkit browsers, tested only on iPhone 3.x so
|
||||
* far.
|
||||
*
|
||||
* Click Behavior:
|
||||
* Not yet implemented.
|
||||
*
|
||||
* Zoom Behavior:
|
||||
* Not yet implemented.
|
||||
*
|
||||
* Swipe Behavior:
|
||||
* Not yet implemented.
|
||||
*/
|
||||
/// Touch Handler. Class that handles all touch events and
|
||||
/// uses them to interpret higher level gestures and behaviors. TouchEvent is a
|
||||
/// built in mobile safari type:
|
||||
/// [http://developer.apple.com/safari/library/documentation/UserExperience/Reference/TouchEventClassReference/TouchEvent/TouchEvent.html].
|
||||
///
|
||||
/// Examples of higher level gestures this class is intended to support
|
||||
/// - click, double click, long click
|
||||
/// - dragging, swiping, zooming
|
||||
///
|
||||
/// Touch Behavior:
|
||||
/// Use this class to make your elements 'touchable' (see Touchable.dart).
|
||||
/// Intended to work with all webkit browsers.
|
||||
///
|
||||
/// Drag Behavior:
|
||||
/// Use this class to make your elements 'draggable' (see draggable.js).
|
||||
/// This behavior will handle all of the required events and report the
|
||||
/// properties of the drag to you while the touch is happening and at the
|
||||
/// end of the drag sequence. This behavior will NOT perform the actual
|
||||
/// dragging (redrawing the element) for you, this responsibility is left to
|
||||
/// the client code. This behavior contains a work around for a mobile
|
||||
/// safari bug where the 'touchend' event is not dispatched when the touch
|
||||
/// goes past the bottom of the browser window.
|
||||
/// This is intended to work well in iframes.
|
||||
/// Intended to work with all webkit browsers, tested only on iPhone 3.x so
|
||||
/// far.
|
||||
///
|
||||
/// Click Behavior:
|
||||
/// Not yet implemented.
|
||||
///
|
||||
/// Zoom Behavior:
|
||||
/// Not yet implemented.
|
||||
///
|
||||
/// Swipe Behavior:
|
||||
/// Not yet implemented.
|
||||
class TouchHandler {
|
||||
Touchable _touchable;
|
||||
final Touchable _touchable;
|
||||
Element _element;
|
||||
|
||||
/** The absolute sum of all touch y deltas. */
|
||||
/// The absolute sum of all touch y deltas. */
|
||||
int _totalMoveY;
|
||||
|
||||
/** The absolute sum of all touch x deltas. */
|
||||
/// The absolute sum of all touch x deltas. */
|
||||
int _totalMoveX;
|
||||
|
||||
/**
|
||||
* A list of tuples where the first item is the horizontal component of a
|
||||
* recent relevant touch and the second item is the touch's time stamp. Old
|
||||
* touches are removed based on the max tracking time and when direction
|
||||
* changes.
|
||||
*/
|
||||
/// A list of tuples where the first item is the horizontal component of a
|
||||
/// recent relevant touch and the second item is the touch's time stamp. Old
|
||||
/// touches are removed based on the max tracking time and when direction
|
||||
/// changes.
|
||||
List<int> _recentTouchesX;
|
||||
|
||||
/**
|
||||
* A list of tuples where the first item is the vertical component of a
|
||||
* recent relevant touch and the second item is the touch's time stamp. Old
|
||||
* touches are removed based on the max tracking time and when direction
|
||||
* changes.
|
||||
*/
|
||||
/// A list of tuples where the first item is the vertical component of a
|
||||
/// recent relevant touch and the second item is the touch's time stamp. Old
|
||||
/// touches are removed based on the max tracking time and when direction
|
||||
/// changes.
|
||||
List<int> _recentTouchesY;
|
||||
|
||||
// TODO(jacobr): make customizable by passing optional parameters to the
|
||||
// TouchHandler constructor.
|
||||
/**
|
||||
* Minimum movement of touch required to be considered a drag.
|
||||
*/
|
||||
/// Minimum movement of touch required to be considered a drag.
|
||||
static const _MIN_TRACKING_FOR_DRAG = 2;
|
||||
|
||||
/**
|
||||
* The maximum number of ms to track a touch event. After an event is older
|
||||
* than this value, it will be ignored in velocity calculations.
|
||||
*/
|
||||
/// The maximum number of ms to track a touch event. After an event is older
|
||||
/// than this value, it will be ignored in velocity calculations.
|
||||
static const _MAX_TRACKING_TIME = 250;
|
||||
|
||||
/** The maximum number of touches to track. */
|
||||
/// The maximum number of touches to track. */
|
||||
static const _MAX_TRACKING_TOUCHES = 5;
|
||||
|
||||
/**
|
||||
* The maximum velocity to return, in pixels per millisecond, that is used to
|
||||
* guard against errors in calculating end velocity of a drag. This is a very
|
||||
* fast drag velocity.
|
||||
*/
|
||||
/// The maximum velocity to return, in pixels per millisecond, that is used to
|
||||
/// guard against errors in calculating end velocity of a drag. This is a very
|
||||
/// fast drag velocity.
|
||||
static const _MAXIMUM_VELOCITY = 5;
|
||||
|
||||
/**
|
||||
* The velocity to return, in pixel per millisecond, when the time stamps on
|
||||
* the events are erroneous. The browser can return bad time stamps if the
|
||||
* thread is blocked for the duration of the drag. This is a low velocity to
|
||||
* prevent the content from moving quickly after a slow drag. It is less
|
||||
* jarring if the content moves slowly after a fast drag.
|
||||
*/
|
||||
/// The velocity to return, in pixel per millisecond, when the time stamps on
|
||||
/// the events are erroneous. The browser can return bad time stamps if the
|
||||
/// thread is blocked for the duration of the drag. This is a low velocity to
|
||||
/// prevent the content from moving quickly after a slow drag. It is less
|
||||
/// jarring if the content moves slowly after a fast drag.
|
||||
static const _VELOCITY_FOR_INCORRECT_EVENTS = 1;
|
||||
|
||||
Draggable _draggable;
|
||||
|
@ -116,30 +102,26 @@ class TouchHandler {
|
|||
int _endTouchX;
|
||||
int _endTouchY;
|
||||
|
||||
TouchHandler(Touchable touchable, [Element element = null])
|
||||
TouchHandler(Touchable touchable, [Element element])
|
||||
: _touchable = touchable,
|
||||
_totalMoveY = 0,
|
||||
_totalMoveX = 0,
|
||||
_recentTouchesX = new List<int>(),
|
||||
_recentTouchesY = new List<int>(),
|
||||
_recentTouchesX = <int>[],
|
||||
_recentTouchesY = <int>[],
|
||||
// TODO(jmesserly): I don't like having to initialize all booleans here
|
||||
// See b/5045736
|
||||
_dragging = false,
|
||||
_tracking = false,
|
||||
_touching = false {
|
||||
_element = element != null ? element : touchable.getElement();
|
||||
_element = element ?? touchable.getElement();
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin tracking the touchable element, it is eligible for dragging.
|
||||
*/
|
||||
/// Begin tracking the touchable element, it is eligible for dragging.
|
||||
void _beginTracking() {
|
||||
_tracking = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop tracking the touchable element, it is no longer dragging.
|
||||
*/
|
||||
/// Stop tracking the touchable element, it is no longer dragging.
|
||||
void _endTracking() {
|
||||
_tracking = false;
|
||||
_dragging = false;
|
||||
|
@ -147,11 +129,9 @@ class TouchHandler {
|
|||
_totalMoveX = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct erroneous velocities by capping the velocity if we think it's too
|
||||
* high, or setting it to a default velocity if know that the event data is
|
||||
* bad. Returns the corrected velocity.
|
||||
*/
|
||||
/// Correct erroneous velocities by capping the velocity if we think it's too
|
||||
/// high, or setting it to a default velocity if know that the event data is
|
||||
/// bad. Returns the corrected velocity.
|
||||
num _correctVelocity(num velocity) {
|
||||
num absVelocity = velocity.abs();
|
||||
if (absVelocity > _MAXIMUM_VELOCITY) {
|
||||
|
@ -162,15 +142,14 @@ class TouchHandler {
|
|||
return absVelocity * (velocity < 0 ? -1 : 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start listenting for events.
|
||||
* If [capture] is True the TouchHandler should listen during the capture
|
||||
* phase.
|
||||
*/
|
||||
/// Start listenting for events.
|
||||
/// If [capture] is True the TouchHandler should listen during the capture
|
||||
/// phase.
|
||||
void enable([bool capture = false]) {
|
||||
Function onEnd = (e) {
|
||||
onEnd(e) {
|
||||
_onEnd(e.timeStamp.toInt(), e);
|
||||
};
|
||||
}
|
||||
|
||||
_addEventListeners(_element, (e) {
|
||||
_onStart(e);
|
||||
}, (e) {
|
||||
|
@ -178,67 +157,55 @@ class TouchHandler {
|
|||
}, onEnd, onEnd, capture);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current horizontal drag delta. Drag delta is defined as the deltaX
|
||||
* of the start touch position and the last touch position.
|
||||
*/
|
||||
/// Get the current horizontal drag delta. Drag delta is defined as the deltaX
|
||||
/// of the start touch position and the last touch position.
|
||||
int getDragDeltaX() {
|
||||
return _lastTouchX - _startTouchX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current vertical drag delta. Drag delta is defined as the deltaY of
|
||||
* the start touch position and the last touch position.
|
||||
*/
|
||||
/// Get the current vertical drag delta. Drag delta is defined as the deltaY of
|
||||
/// the start touch position and the last touch position.
|
||||
int getDragDeltaY() {
|
||||
return _lastTouchY - _startTouchY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get end velocity of the drag. This method is specific to drag behavior, so
|
||||
* if touch behavior and drag behavior is split then this should go with drag
|
||||
* behavior. End velocity is defined as deltaXY / deltaTime where deltaXY is
|
||||
* the difference between endPosition and the oldest recent position, and
|
||||
* deltaTime is the difference between endTime and the oldest recent time
|
||||
* stamp.
|
||||
*/
|
||||
/// Get end velocity of the drag. This method is specific to drag behavior, so
|
||||
/// if touch behavior and drag behavior is split then this should go with drag
|
||||
/// behavior. End velocity is defined as deltaXY / deltaTime where deltaXY is
|
||||
/// the difference between endPosition and the oldest recent position, and
|
||||
/// deltaTime is the difference between endTime and the oldest recent time
|
||||
/// stamp.
|
||||
Coordinate getEndVelocity() {
|
||||
num velocityX = 0;
|
||||
num velocityY = 0;
|
||||
|
||||
if (_recentTouchesX.length > 0) {
|
||||
if (_recentTouchesX.isNotEmpty) {
|
||||
num timeDeltaX = Math.max(1, _endTime - _recentTouchesX[1]);
|
||||
velocityX = (_endTouchX - _recentTouchesX[0]) / timeDeltaX;
|
||||
}
|
||||
|
||||
if (_recentTouchesY.length > 0) {
|
||||
if (_recentTouchesY.isNotEmpty) {
|
||||
num timeDeltaY = Math.max(1, _endTime - _recentTouchesY[1]);
|
||||
velocityY = (_endTouchY - _recentTouchesY[0]) / timeDeltaY;
|
||||
}
|
||||
velocityX = _correctVelocity(velocityX);
|
||||
velocityY = _correctVelocity(velocityY);
|
||||
return new Coordinate(velocityX, velocityY);
|
||||
return Coordinate(velocityX, velocityY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the touch of the last event.
|
||||
*/
|
||||
/// Return the touch of the last event.
|
||||
Touch _getLastTouch() {
|
||||
assert(_lastEvent != null); // Last event not set
|
||||
return _lastEvent.touches[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Is the touch manager currently tracking touch moves to detect a drag?
|
||||
*/
|
||||
/// Is the touch manager currently tracking touch moves to detect a drag?
|
||||
bool isTracking() {
|
||||
return _tracking;
|
||||
}
|
||||
|
||||
/**
|
||||
* Touch end handler.
|
||||
*/
|
||||
void _onEnd(int timeStamp, [TouchEvent e = null]) {
|
||||
/// Touch end handler.
|
||||
void _onEnd(int timeStamp, [TouchEvent e]) {
|
||||
_touching = false;
|
||||
_touchable.onTouchEnd();
|
||||
if (!_tracking || _draggable == null) {
|
||||
|
@ -262,9 +229,7 @@ class TouchHandler {
|
|||
_endTracking();
|
||||
}
|
||||
|
||||
/**
|
||||
* Touch move handler.
|
||||
*/
|
||||
/// Touch move handler.
|
||||
void _onMove(TouchEvent e) {
|
||||
if (!_tracking || _draggable == null) {
|
||||
return;
|
||||
|
@ -311,9 +276,7 @@ class TouchHandler {
|
|||
_lastMoveY = moveY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Touch start handler.
|
||||
*/
|
||||
/// Touch start handler.
|
||||
void _onStart(TouchEvent e) {
|
||||
if (_touching) {
|
||||
return;
|
||||
|
@ -328,8 +291,8 @@ class TouchHandler {
|
|||
int timeStamp = e.timeStamp.toInt();
|
||||
_startTime = timeStamp;
|
||||
// TODO(jacobr): why don't we just clear the lists?
|
||||
_recentTouchesX = new List<int>();
|
||||
_recentTouchesY = new List<int>();
|
||||
_recentTouchesX = <int>[];
|
||||
_recentTouchesY = <int>[];
|
||||
_recentTouchesX.add(touch.client.x);
|
||||
_recentTouchesX.add(timeStamp);
|
||||
_recentTouchesY.add(touch.client.y);
|
||||
|
@ -338,13 +301,11 @@ class TouchHandler {
|
|||
_beginTracking();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the provided recent touches list to remove all touches older than
|
||||
* the max tracking time or the 5th most recent touch.
|
||||
* [recentTouches] specifies a list of tuples where the first item is the x
|
||||
* or y component of the recent touch and the second item is the touch time
|
||||
* stamp. The time of the most recent event is specified by [recentTime].
|
||||
*/
|
||||
/// Filters the provided recent touches list to remove all touches older than
|
||||
/// the max tracking time or the 5th most recent touch.
|
||||
/// [recentTouches] specifies a list of tuples where the first item is the x
|
||||
/// or y component of the recent touch and the second item is the touch time
|
||||
/// stamp. The time of the most recent event is specified by [recentTime].
|
||||
List<int> _removeOldTouches(List<int> recentTouches, int recentTime) {
|
||||
int count = 0;
|
||||
final len = recentTouches.length;
|
||||
|
@ -361,14 +322,12 @@ class TouchHandler {
|
|||
return list.sublist(n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the provided recent touches list to remove all touches except the
|
||||
* last if the move direction has changed.
|
||||
* [recentTouches] specifies a list of tuples where the first item is the x
|
||||
* or y component of the recent touch and the second item is the touch time
|
||||
* stamp. The x or y component of the most recent move is specified by
|
||||
* [recentMove].
|
||||
*/
|
||||
/// Filters the provided recent touches list to remove all touches except the
|
||||
/// last if the move direction has changed.
|
||||
/// [recentTouches] specifies a list of tuples where the first item is the x
|
||||
/// or y component of the recent touch and the second item is the touch time
|
||||
/// stamp. The x or y component of the most recent move is specified by
|
||||
/// [recentMove].
|
||||
List<int> _removeTouchesInWrongDirection(
|
||||
List<int> recentTouches, int lastMove, int recentMove) {
|
||||
if (lastMove != 0 &&
|
||||
|
@ -383,19 +342,15 @@ class TouchHandler {
|
|||
// TODO(jacobr): why doesn't bool implement the xor operator directly?
|
||||
static bool _xor(bool a, bool b) => a != b;
|
||||
|
||||
/**
|
||||
* Reset the touchable element.
|
||||
*/
|
||||
/// Reset the touchable element.
|
||||
void reset() {
|
||||
_endTracking();
|
||||
_touching = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this method to enable drag behavior on a draggable delegate.
|
||||
* The [draggable] object can be the same as the [_touchable] object, they are
|
||||
* assigned to different members to allow for strong typing with interfaces.
|
||||
*/
|
||||
/// Call this method to enable drag behavior on a draggable delegate.
|
||||
/// The [draggable] object can be the same as the [_touchable] object, they are
|
||||
/// assigned to different members to allow for strong typing with interfaces.
|
||||
void setDraggable(Draggable draggable) {
|
||||
_draggable = draggable;
|
||||
}
|
||||
|
|
|
@ -6,17 +6,15 @@
|
|||
|
||||
part of touch;
|
||||
|
||||
/**
|
||||
* Wraps a callback with translations of mouse events to touch events. Use
|
||||
* this function to invoke your callback that expects touch events after
|
||||
* touch events are created from the actual mouse events.
|
||||
*/
|
||||
/// Wraps a callback with translations of mouse events to touch events. Use
|
||||
/// this function to invoke your callback that expects touch events after
|
||||
/// touch events are created from the actual mouse events.
|
||||
EventListener mouseToTouchCallback(EventListener callback) {
|
||||
return (Event e) {
|
||||
var touches = <Touch>[];
|
||||
var targetTouches = <Touch>[];
|
||||
var changedTouches = <Touch>[];
|
||||
final mockTouch = new MockTouch(e);
|
||||
final mockTouch = MockTouch(e);
|
||||
final mockTouchList = <Touch>[mockTouch];
|
||||
if (e.type == 'mouseup') {
|
||||
changedTouches = mockTouchList;
|
||||
|
@ -24,14 +22,14 @@ EventListener mouseToTouchCallback(EventListener callback) {
|
|||
touches = mockTouchList;
|
||||
targetTouches = mockTouchList;
|
||||
}
|
||||
callback(new MockTouchEvent(e, touches, targetTouches, changedTouches));
|
||||
callback(MockTouchEvent(e, touches, targetTouches, changedTouches));
|
||||
// Required to prevent spurious selection changes while tracking touches
|
||||
// on devices that don't support touch events.
|
||||
e.preventDefault();
|
||||
};
|
||||
}
|
||||
|
||||
/** Helper method to attach event listeners to a [node]. */
|
||||
/// Helper method to attach event listeners to a [node]. */
|
||||
void _addEventListeners(Element node, EventListener onStart,
|
||||
EventListener onMove, EventListener onEnd, EventListener onCancel,
|
||||
[bool capture = false]) {
|
||||
|
@ -53,10 +51,10 @@ void _addEventListeners(Element node, EventListener onStart,
|
|||
}
|
||||
|
||||
if (Device.supportsTouch) {
|
||||
var touchMoveSub;
|
||||
var touchEndSub;
|
||||
var touchLeaveSub;
|
||||
var touchCancelSub;
|
||||
StreamSubscription<TouchEvent> touchMoveSub;
|
||||
StreamSubscription<TouchEvent> touchEndSub;
|
||||
StreamSubscription<TouchEvent> touchLeaveSub;
|
||||
StreamSubscription<TouchEvent> touchCancelSub;
|
||||
|
||||
removeListeners = () {
|
||||
touchMoveSub.cancel();
|
||||
|
@ -86,9 +84,9 @@ void _addEventListeners(Element node, EventListener onStart,
|
|||
onEnd = mouseToTouchCallback(onEnd);
|
||||
// onLeave will never be called if the device does not support touches.
|
||||
|
||||
var mouseMoveSub;
|
||||
var mouseUpSub;
|
||||
var touchCancelSub;
|
||||
StreamSubscription<MouseEvent> mouseMoveSub;
|
||||
StreamSubscription<MouseEvent> mouseUpSub;
|
||||
StreamSubscription<TouchEvent> touchCancelSub;
|
||||
|
||||
removeListeners = () {
|
||||
mouseMoveSub.cancel();
|
||||
|
@ -111,10 +109,8 @@ void _addEventListeners(Element node, EventListener onStart,
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether the given touch event targets the node, or one of the node's
|
||||
* children.
|
||||
*/
|
||||
/// Gets whether the given touch event targets the node, or one of the node's
|
||||
/// children.
|
||||
bool _touchEventTargetsNode(event, Node node) {
|
||||
Node target = event.changedTouches[0].target;
|
||||
|
||||
|
@ -131,40 +127,28 @@ bool _touchEventTargetsNode(event, Node node) {
|
|||
}
|
||||
|
||||
abstract class Touchable {
|
||||
/**
|
||||
* Provide the HTML element that should respond to touch events.
|
||||
*/
|
||||
/// Provide the HTML element that should respond to touch events.
|
||||
Element getElement();
|
||||
|
||||
/**
|
||||
* The object has received a touchend event.
|
||||
*/
|
||||
/// The object has received a touchend event.
|
||||
void onTouchEnd();
|
||||
|
||||
/**
|
||||
* The object has received a touchstart event.
|
||||
* Returns return true if you want to allow a drag sequence to begin,
|
||||
* false you want to disable dragging for the duration of this touch.
|
||||
*/
|
||||
/// The object has received a touchstart event.
|
||||
/// Returns return true if you want to allow a drag sequence to begin,
|
||||
/// false you want to disable dragging for the duration of this touch.
|
||||
bool onTouchStart(TouchEvent e);
|
||||
}
|
||||
|
||||
abstract class Draggable implements Touchable {
|
||||
/**
|
||||
* The object's drag sequence is now complete.
|
||||
*/
|
||||
/// The object's drag sequence is now complete.
|
||||
void onDragEnd();
|
||||
|
||||
/**
|
||||
* The object has been dragged to a new position.
|
||||
*/
|
||||
/// The object has been dragged to a new position.
|
||||
void onDragMove();
|
||||
|
||||
/**
|
||||
* The object has started dragging.
|
||||
* Returns true to allow a drag sequence to begin (custom behavior),
|
||||
* false to disable dragging for this touch duration (allow native scrolling).
|
||||
*/
|
||||
/// The object has started dragging.
|
||||
/// Returns true to allow a drag sequence to begin (custom behavior),
|
||||
/// false to disable dragging for this touch duration (allow native scrolling).
|
||||
bool onDragStart(TouchEvent e);
|
||||
|
||||
bool get verticalEnabled;
|
||||
|
@ -174,14 +158,16 @@ abstract class Draggable implements Touchable {
|
|||
class MockTouch implements Touch {
|
||||
MouseEvent wrapped;
|
||||
|
||||
MockTouch(MouseEvent this.wrapped) {}
|
||||
MockTouch(this.wrapped);
|
||||
|
||||
int get clientX => wrapped.client.x;
|
||||
|
||||
int get clientY => wrapped.client.y;
|
||||
|
||||
@override
|
||||
get client => wrapped.client;
|
||||
|
||||
@override
|
||||
int get identifier => 0;
|
||||
|
||||
int get pageX => wrapped.page.x;
|
||||
|
@ -194,50 +180,58 @@ class MockTouch implements Touch {
|
|||
return wrapped.screen.y;
|
||||
}
|
||||
|
||||
@override
|
||||
EventTarget get target => wrapped.target;
|
||||
|
||||
@override
|
||||
double get force {
|
||||
throw new UnimplementedError();
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Point get page {
|
||||
throw new UnimplementedError();
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
int get radiusX {
|
||||
throw new UnimplementedError();
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
int get radiusY {
|
||||
throw new UnimplementedError();
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
String get region {
|
||||
throw new UnimplementedError();
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
num get rotationAngle {
|
||||
throw new UnimplementedError();
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Point get screen {
|
||||
throw new UnimplementedError();
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
num get webkitForce {
|
||||
throw new UnimplementedError();
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
int get webkitRadiusX {
|
||||
throw new UnimplementedError();
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
int get webkitRadiusY {
|
||||
throw new UnimplementedError();
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
num get webkitRotationAngle {
|
||||
throw new UnimplementedError();
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -250,75 +244,95 @@ class MockTouchList extends Object
|
|||
|
||||
static bool get supported => true;
|
||||
|
||||
@override
|
||||
int get length => values.length;
|
||||
|
||||
@override
|
||||
Touch operator [](int index) => values[index];
|
||||
|
||||
@override
|
||||
void operator []=(int index, Touch value) {
|
||||
throw new UnsupportedError("Cannot assign element of immutable List.");
|
||||
throw UnsupportedError("Cannot assign element of immutable List.");
|
||||
}
|
||||
|
||||
@override
|
||||
set length(int value) {
|
||||
throw new UnsupportedError("Cannot resize immutable List.");
|
||||
throw UnsupportedError("Cannot resize immutable List.");
|
||||
}
|
||||
|
||||
@override
|
||||
Touch item(int index) => values[index];
|
||||
}
|
||||
|
||||
class MockTouchEvent implements TouchEvent {
|
||||
dynamic /*MouseEvent*/ wrapped;
|
||||
@override
|
||||
final TouchList touches;
|
||||
@override
|
||||
final TouchList targetTouches;
|
||||
@override
|
||||
final TouchList changedTouches;
|
||||
MockTouchEvent(MouseEvent this.wrapped, List<Touch> touches,
|
||||
List<Touch> targetTouches, List<Touch> changedTouches)
|
||||
: touches = new MockTouchList(touches),
|
||||
targetTouches = new MockTouchList(targetTouches),
|
||||
changedTouches = new MockTouchList(changedTouches);
|
||||
: touches = MockTouchList(touches),
|
||||
targetTouches = MockTouchList(targetTouches),
|
||||
changedTouches = MockTouchList(changedTouches);
|
||||
|
||||
@override
|
||||
bool get bubbles => wrapped.bubbles;
|
||||
|
||||
bool get cancelBubble => wrapped.cancelBubble;
|
||||
|
||||
void set cancelBubble(bool value) {
|
||||
set cancelBubble(bool value) {
|
||||
wrapped.cancelBubble = value;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get cancelable => wrapped.cancelable;
|
||||
|
||||
@override
|
||||
EventTarget get currentTarget => wrapped.currentTarget;
|
||||
|
||||
@override
|
||||
bool get defaultPrevented => wrapped.defaultPrevented;
|
||||
|
||||
@override
|
||||
int get eventPhase => wrapped.eventPhase;
|
||||
|
||||
void set returnValue(bool value) {
|
||||
set returnValue(bool value) {
|
||||
wrapped.returnValue = value;
|
||||
}
|
||||
|
||||
bool get returnValue => wrapped.returnValue;
|
||||
|
||||
@override
|
||||
EventTarget get target => wrapped.target;
|
||||
|
||||
/*At different times, int, double, and String*/
|
||||
@override
|
||||
get timeStamp => wrapped.timeStamp;
|
||||
|
||||
@override
|
||||
String get type => wrapped.type;
|
||||
|
||||
@override
|
||||
void preventDefault() {
|
||||
wrapped.preventDefault();
|
||||
}
|
||||
|
||||
@override
|
||||
void stopImmediatePropagation() {
|
||||
wrapped.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
@override
|
||||
void stopPropagation() {
|
||||
wrapped.stopPropagation();
|
||||
}
|
||||
|
||||
int get charCode => wrapped.charCode;
|
||||
|
||||
@override
|
||||
int get detail => wrapped.detail;
|
||||
|
||||
// TODO(sra): keyCode is not on MouseEvent.
|
||||
|
@ -332,67 +346,78 @@ class MockTouchEvent implements TouchEvent {
|
|||
|
||||
int get pageY => wrapped.page.y;
|
||||
|
||||
@override
|
||||
Window get view => wrapped.view;
|
||||
|
||||
int get which => wrapped.which;
|
||||
|
||||
@override
|
||||
bool get altKey => wrapped.altKey;
|
||||
|
||||
@override
|
||||
bool get ctrlKey => wrapped.ctrlKey;
|
||||
|
||||
@override
|
||||
bool get metaKey => wrapped.metaKey;
|
||||
|
||||
@override
|
||||
bool get shiftKey => wrapped.shiftKey;
|
||||
|
||||
DataTransfer get clipboardData {
|
||||
throw new UnimplementedError();
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
List<EventTarget> deepPath() {
|
||||
throw new UnimplementedError();
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get isTrusted {
|
||||
throw new UnimplementedError();
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
Point get layer {
|
||||
throw new UnimplementedError();
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Element get matchingTarget {
|
||||
throw new UnimplementedError();
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
Point get page {
|
||||
throw new UnimplementedError();
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
List<EventTarget> get path {
|
||||
throw new UnimplementedError();
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
bool get scoped {
|
||||
throw new UnimplementedError();
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
Point get screen {
|
||||
throw new UnimplementedError();
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
/*InputDeviceCapabilities*/ get sourceCapabilities {
|
||||
throw new UnimplementedError();
|
||||
/*InputDeviceCapabilities*/ @override
|
||||
get sourceCapabilities {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
/*InputDevice*/ get sourceDevice {
|
||||
throw new UnimplementedError();
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
bool get composed {
|
||||
throw new UnimplementedError();
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
List<EventTarget> composedPath() {
|
||||
throw new UnimplementedError();
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,12 +4,10 @@
|
|||
|
||||
part of utilslib;
|
||||
|
||||
typedef int NumericValueSelector<T>(T value);
|
||||
typedef NumericValueSelector<T> = int Function(T value);
|
||||
|
||||
/**
|
||||
* General purpose collection utilities.
|
||||
* TODO(jmesserly): make these top level functions?
|
||||
*/
|
||||
/// General purpose collection utilities.
|
||||
/// TODO(jmesserly): make these top level functions?
|
||||
class CollectionUtils {
|
||||
static void insertAt(List arr, int pos, value) {
|
||||
assert(pos >= 0);
|
||||
|
@ -39,12 +37,10 @@ class CollectionUtils {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the item in [source] that matches [test]. Returns null if
|
||||
* no item matches. The typing should be:
|
||||
* T find(Iterable<T> source, bool test(T item)), but we don't have generic
|
||||
* functions.
|
||||
*/
|
||||
/// Finds the item in [source] that matches [test]. Returns null if
|
||||
/// no item matches. The typing should be:
|
||||
/// T find(Iterable<T> source, bool test(T item)), but we don't have generic
|
||||
/// functions.
|
||||
static find(Iterable source, bool test(item)) {
|
||||
for (final item in source) {
|
||||
if (test(item)) return item;
|
||||
|
@ -53,7 +49,7 @@ class CollectionUtils {
|
|||
return null;
|
||||
}
|
||||
|
||||
/** Compute the minimum of an iterable. Returns null if empty. */
|
||||
/// Compute the minimum of an iterable. Returns null if empty. */
|
||||
static num? min(Iterable source) {
|
||||
final iter = source.iterator;
|
||||
if (!iter.moveNext()) {
|
||||
|
@ -66,7 +62,7 @@ class CollectionUtils {
|
|||
return best;
|
||||
}
|
||||
|
||||
/** Compute the maximum of an iterable. Returns null if empty. */
|
||||
/// Compute the maximum of an iterable. Returns null if empty. */
|
||||
static num? max(Iterable source) {
|
||||
final iter = source.iterator;
|
||||
if (!iter.moveNext()) {
|
||||
|
@ -79,19 +75,19 @@ class CollectionUtils {
|
|||
return best;
|
||||
}
|
||||
|
||||
/** Orders an iterable by its values, or by a key selector. */
|
||||
/// Orders an iterable by its values, or by a key selector. */
|
||||
static List<T> orderBy<T>(Iterable<T> source,
|
||||
[NumericValueSelector? selector = null]) {
|
||||
[NumericValueSelector? selector]) {
|
||||
final result = List<T>.from(source);
|
||||
sortBy(result, selector);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Sorts a list by its values, or by a key selector. */
|
||||
/// Sorts a list by its values, or by a key selector. */
|
||||
// TODO(jmesserly): we probably don't want to call the key selector more than
|
||||
// once for a given element. This would improve performance and the API
|
||||
// contract could be stronger.
|
||||
static void sortBy(List list, [NumericValueSelector? selector = null]) {
|
||||
static void sortBy(List list, [NumericValueSelector? selector]) {
|
||||
if (selector != null) {
|
||||
list.sort((x, y) => selector(x) - selector(y));
|
||||
} else {
|
||||
|
@ -99,8 +95,8 @@ class CollectionUtils {
|
|||
}
|
||||
}
|
||||
|
||||
/** Compute the sum of an iterable. An empty iterable is an error. */
|
||||
static num sum(Iterable source, [NumericValueSelector? selector = null]) {
|
||||
/// Compute the sum of an iterable. An empty iterable is an error. */
|
||||
static num sum(Iterable source, [NumericValueSelector? selector]) {
|
||||
final iter = source.iterator;
|
||||
bool wasEmpty = true;
|
||||
num total = 0;
|
||||
|
|
|
@ -4,12 +4,10 @@
|
|||
|
||||
part of utilslib;
|
||||
|
||||
/**
|
||||
* General purpose date/time utilities.
|
||||
*/
|
||||
/// General purpose date/time utilities.
|
||||
class DateUtils {
|
||||
// TODO(jmesserly): localized strings
|
||||
static const WEEKDAYS = const [
|
||||
static const WEEKDAYS = [
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
|
@ -83,7 +81,7 @@ class DateUtils {
|
|||
return result.toLocal();
|
||||
}
|
||||
|
||||
/** Parse a string like: 2011-07-19T22:03:04.000Z */
|
||||
/// Parse a string like: 2011-07-19T22:03:04.000Z */
|
||||
// TODO(jmesserly): workaround for DateTime.fromDate, which has issues:
|
||||
// * on Dart VM it doesn't handle all of ISO 8601. See b/5055106.
|
||||
// * on DartC it doesn't work on Safari. See b/5062557.
|
||||
|
@ -110,7 +108,7 @@ class DateUtils {
|
|||
ensure(time.length == 3);
|
||||
|
||||
final seconds = time[2].split('.');
|
||||
ensure(seconds.length >= 1 && seconds.length <= 2);
|
||||
ensure(seconds.isNotEmpty && seconds.length <= 2);
|
||||
int milliseconds = 0;
|
||||
if (seconds.length == 2) {
|
||||
milliseconds = int.parse(seconds[1]);
|
||||
|
@ -126,13 +124,11 @@ class DateUtils {
|
|||
milliseconds);
|
||||
}
|
||||
|
||||
/**
|
||||
* A date/time formatter that takes into account the current date/time:
|
||||
* - if it's from today, just show the time
|
||||
* - if it's from yesterday, just show 'Yesterday'
|
||||
* - if it's from the same week, just show the weekday
|
||||
* - otherwise, show just the date
|
||||
*/
|
||||
/// A date/time formatter that takes into account the current date/time:
|
||||
/// - if it's from today, just show the time
|
||||
/// - if it's from yesterday, just show 'Yesterday'
|
||||
/// - if it's from the same week, just show the weekday
|
||||
/// - otherwise, show just the date
|
||||
static String toRecentTimeString(DateTime then) {
|
||||
bool datesAreEqual(DateTime d1, DateTime d2) {
|
||||
return (d1.year == d2.year) &&
|
||||
|
@ -159,13 +155,13 @@ class DateUtils {
|
|||
} else {
|
||||
// TODO(jmesserly): locale specific date format
|
||||
String twoDigits(int n) {
|
||||
if (n >= 10) return "${n}";
|
||||
return "0${n}";
|
||||
if (n >= 10) return "$n";
|
||||
return "0$n";
|
||||
}
|
||||
|
||||
String twoDigitMonth = twoDigits(then.month);
|
||||
String twoDigitDay = twoDigits(then.day);
|
||||
return "${then.year}-${twoDigitMonth}-${twoDigitDay}";
|
||||
return "${then.year}-$twoDigitMonth-$twoDigitDay";
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -179,7 +175,7 @@ class DateUtils {
|
|||
return ((daysSince1970 + DateTime.thursday) % DateTime.daysPerWeek);
|
||||
}
|
||||
|
||||
/** Formats a time in H:MM A format */
|
||||
/// Formats a time in H:MM A format */
|
||||
// TODO(jmesserly): should get 12 vs 24 hour clock setting from the locale
|
||||
static String toHourMinutesString(Duration duration) {
|
||||
assert(duration.inDays == 0);
|
||||
|
@ -197,12 +193,12 @@ class DateUtils {
|
|||
}
|
||||
}
|
||||
String twoDigits(int n) {
|
||||
if (n >= 10) return "${n}";
|
||||
return "0${n}";
|
||||
if (n >= 10) return "$n";
|
||||
return "0$n";
|
||||
}
|
||||
|
||||
String mm =
|
||||
twoDigits(duration.inMinutes.remainder(Duration.minutesPerHour));
|
||||
return "${hours}:${mm} ${a}";
|
||||
return "$hours:$mm $a";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,30 +4,26 @@
|
|||
|
||||
part of utilslib;
|
||||
|
||||
/**
|
||||
* General purpose string manipulation utilities.
|
||||
*/
|
||||
/// General purpose string manipulation utilities.
|
||||
class StringUtils {
|
||||
/**
|
||||
* Returns either [str], or if [str] is null, the value of [defaultStr].
|
||||
*/
|
||||
/// Returns either [str], or if [str] is null, the value of [defaultStr].
|
||||
static String defaultString(String? str, [String defaultStr = '']) {
|
||||
return str == null ? defaultStr : str;
|
||||
return str ?? defaultStr;
|
||||
}
|
||||
|
||||
/** Parse string to a double, and handle null intelligently */
|
||||
static double? parseDouble(String? str, [double? ifNull = null]) {
|
||||
/// Parse string to a double, and handle null intelligently */
|
||||
static double? parseDouble(String? str, [double? ifNull]) {
|
||||
return (str == null) ? ifNull : double.parse(str);
|
||||
}
|
||||
|
||||
/** Parse string to a int, and handle null intelligently */
|
||||
static int? parseInt(String? str, [int? ifNull = null]) {
|
||||
/// Parse string to a int, and handle null intelligently */
|
||||
static int? parseInt(String? str, [int? ifNull]) {
|
||||
return (str == null) ? ifNull : int.parse(str);
|
||||
}
|
||||
|
||||
/** Parse bool to a double, and handle null intelligently */
|
||||
/// Parse bool to a double, and handle null intelligently */
|
||||
// TODO(jacobr): corelib should have a boolean parsing method
|
||||
static bool? parseBool(String? str, [bool? ifNull = null]) {
|
||||
static bool? parseBool(String? str, [bool? ifNull]) {
|
||||
assert(str == null || str == 'true' || str == 'false');
|
||||
return (str == null) ? ifNull : (str == 'true');
|
||||
}
|
||||
|
|
|
@ -4,20 +4,16 @@
|
|||
|
||||
part of utilslib;
|
||||
|
||||
/**
|
||||
* A parsed URI, inspired by:
|
||||
* https://github.com/google/closure-library/blob/master/closure/goog/uri/uri.js
|
||||
*/
|
||||
/// A parsed URI, inspired by:
|
||||
/// https://github.com/google/closure-library/blob/master/closure/goog/uri/uri.js
|
||||
class SwarmUri {
|
||||
/**
|
||||
* Parses a URL query string into a map. Because you can have multiple values
|
||||
* for the same parameter name, each parameter name maps to a list of
|
||||
* values. For example, '?a=b&c=d&a=e' would be parsed as
|
||||
* [{'a':['b','e'],'c':['d']}].
|
||||
*/
|
||||
/// Parses a URL query string into a map. Because you can have multiple values
|
||||
/// for the same parameter name, each parameter name maps to a list of
|
||||
/// values. For example, '?a=b&c=d&a=e' would be parsed as
|
||||
/// [{'a':['b','e'],'c':['d']}].
|
||||
// TODO(jmesserly): consolidate with Uri.parse(...)
|
||||
static Map<String, List<String>> parseQuery(String queryString) {
|
||||
final queryParams = Map<String, List<String>>();
|
||||
final queryParams = <String, List<String>>{};
|
||||
if (queryString.startsWith('?')) {
|
||||
final params = queryString.substring(1, queryString.length).split('&');
|
||||
for (final param in params) {
|
||||
|
@ -41,9 +37,7 @@ class SwarmUri {
|
|||
return queryParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Percent-encodes a string for use as a query parameter in a URI.
|
||||
*/
|
||||
/// Percent-encodes a string for use as a query parameter in a URI.
|
||||
// TODO(rnystrom): Get rid of this when the real encodeURIComponent()
|
||||
// function is available within Dart.
|
||||
static String? encodeComponent(String? component) {
|
||||
|
@ -60,10 +54,8 @@ class SwarmUri {
|
|||
.replaceAll(' ', '%20');
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes a string used a query parameter by replacing percent-encoded
|
||||
* sequences with their original characters.
|
||||
*/
|
||||
/// Decodes a string used a query parameter by replacing percent-encoded
|
||||
/// sequences with their original characters.
|
||||
// TODO(jmesserly): replace this with a better implementation
|
||||
static String? decodeComponent(String? component) {
|
||||
if (component == null) return component;
|
||||
|
|
|
@ -6,10 +6,9 @@
|
|||
|
||||
part of view;
|
||||
|
||||
/**
|
||||
* A View that is composed of child views.
|
||||
*/
|
||||
/// A View that is composed of child views.
|
||||
class CompositeView extends View {
|
||||
@override
|
||||
List<View> childViews;
|
||||
|
||||
// TODO(rnystrom): Allowing this to be public is gross. CompositeView should
|
||||
|
@ -26,7 +25,7 @@ class CompositeView extends View {
|
|||
final bool _nestedContainer;
|
||||
final bool _showScrollbar;
|
||||
|
||||
CompositeView(String this._cssName,
|
||||
CompositeView(this._cssName,
|
||||
[nestedContainer = false,
|
||||
scrollable = false,
|
||||
vertical = false,
|
||||
|
@ -35,26 +34,27 @@ class CompositeView extends View {
|
|||
_scrollable = scrollable,
|
||||
_vertical = vertical,
|
||||
_showScrollbar = showScrollbar,
|
||||
childViews = new List<View>() {}
|
||||
childViews = <View>[];
|
||||
|
||||
@override
|
||||
Element render() {
|
||||
Element node = new Element.html('<div class="$_cssName"></div>');
|
||||
Element node = Element.html('<div class="$_cssName"></div>');
|
||||
|
||||
if (_nestedContainer) {
|
||||
container = new Element.html('<div class="scroll-container"></div>');
|
||||
container = Element.html('<div class="scroll-container"></div>');
|
||||
node.nodes.add(container);
|
||||
} else {
|
||||
container = node;
|
||||
}
|
||||
|
||||
if (_scrollable) {
|
||||
scroller = new Scroller(
|
||||
scroller = Scroller(
|
||||
container,
|
||||
_vertical /* verticalScrollEnabled */,
|
||||
!_vertical /* horizontalScrollEnabled */,
|
||||
true /* momementumEnabled */);
|
||||
if (_showScrollbar) {
|
||||
_scrollbar = new Scrollbar(scroller);
|
||||
_scrollbar = Scrollbar(scroller);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,6 +65,7 @@ class CompositeView extends View {
|
|||
return node;
|
||||
}
|
||||
|
||||
@override
|
||||
void afterRender(Element node) {
|
||||
if (_scrollbar != null) {
|
||||
_scrollbar.initialize();
|
||||
|
|
|
@ -6,11 +6,9 @@
|
|||
|
||||
part of view;
|
||||
|
||||
/**
|
||||
* Holds a number of child views. As you switch between views, the old
|
||||
* view is pushed off to the side and the new view slides in from the other
|
||||
* side.
|
||||
*/
|
||||
/// Holds a number of child views. As you switch between views, the old
|
||||
/// view is pushed off to the side and the new view slides in from the other
|
||||
/// side.
|
||||
class ConveyorView extends CompositeView {
|
||||
// TODO(jmesserly): some places use this property to know when the slide
|
||||
// transition is finished. It would be better to have an event that fires
|
||||
|
@ -29,8 +27,9 @@ class ConveyorView extends CompositeView {
|
|||
|
||||
ConveyorView()
|
||||
: animationTimer = null,
|
||||
super('conveyor-view', true) {}
|
||||
super('conveyor-view', true);
|
||||
|
||||
@override
|
||||
Element render() {
|
||||
final result = super.render();
|
||||
// TODO(rnystrom): Have to do this in render() because container doesn't
|
||||
|
@ -60,11 +59,11 @@ class ConveyorView extends CompositeView {
|
|||
// specified in miliseconds rather than accepting a string.
|
||||
style.transitionDuration = '${durationSeconds}s';
|
||||
final xTranslationPercent = -index * 100;
|
||||
style.transform = 'translate3d(${xTranslationPercent}%, 0px, 0px)';
|
||||
style.transform = 'translate3d($xTranslationPercent%, 0px, 0px)';
|
||||
|
||||
if (animate) {
|
||||
animationTimer = new Timer(
|
||||
new Duration(milliseconds: ((durationSeconds * 1000).toInt())), () {
|
||||
animationTimer =
|
||||
Timer(Duration(milliseconds: ((durationSeconds * 1000).toInt())), () {
|
||||
_onAnimationEnd();
|
||||
});
|
||||
}
|
||||
|
@ -81,10 +80,9 @@ class ConveyorView extends CompositeView {
|
|||
throw "view not found";
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a child view to the ConveyorView. The views are stacked horizontally
|
||||
* in the order they are added.
|
||||
*/
|
||||
/// Adds a child view to the ConveyorView. The views are stacked horizontally
|
||||
/// in the order they are added.
|
||||
@override
|
||||
View addChild(View view) {
|
||||
view.addClass('conveyor-item');
|
||||
view.transform = 'translate3d(${(childViews.length * 100)}%, 0, 0)';
|
||||
|
|
|
@ -8,10 +8,8 @@ part of view;
|
|||
|
||||
// TODO(jacobr): handle splitting lines on symbols such as '-' that aren't
|
||||
// whitespace but are valid word breaking points.
|
||||
/**
|
||||
* Utility class to efficiently word break and measure text without requiring
|
||||
* access to the DOM.
|
||||
*/
|
||||
/// Utility class to efficiently word break and measure text without requiring
|
||||
/// access to the DOM.
|
||||
class MeasureText {
|
||||
static CanvasRenderingContext2D _context;
|
||||
|
||||
|
@ -23,7 +21,7 @@ class MeasureText {
|
|||
|
||||
MeasureText(this.font) {
|
||||
if (_context == null) {
|
||||
CanvasElement canvas = new Element.tag('canvas');
|
||||
CanvasElement canvas = Element.tag('canvas');
|
||||
_context = canvas.getContext('2d');
|
||||
}
|
||||
if (_spaceLength == null) {
|
||||
|
@ -60,12 +58,10 @@ class MeasureText {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add line broken text as html separated by <br> elements.
|
||||
* Returns the number of lines in the output.
|
||||
* This function is safe to call with [:sb == null:] in which case just the
|
||||
* line count is returned.
|
||||
*/
|
||||
/// Add line broken text as html separated by <br> elements.
|
||||
/// Returns the number of lines in the output.
|
||||
/// This function is safe to call with [:sb == null:] in which case just the
|
||||
/// line count is returned.
|
||||
int addLineBrokenText(
|
||||
StringBuffer sb, String text, num lineWidth, int maxLines) {
|
||||
// Strip surrounding whitespace. This ensures we create zero lines if there
|
||||
|
@ -119,8 +115,8 @@ class MeasureText {
|
|||
int lines = 0;
|
||||
num currentLength = 0;
|
||||
int startIndex = 0;
|
||||
int wordStartIndex = null;
|
||||
int lastWordEndIndex = null;
|
||||
int wordStartIndex;
|
||||
int lastWordEndIndex;
|
||||
bool lastWhitespace = true;
|
||||
// TODO(jacobr): optimize this further.
|
||||
// To simplify the logic, we simulate injecting a whitespace character
|
||||
|
|
|
@ -11,25 +11,25 @@ class PageState {
|
|||
final ObservableValue<int> target;
|
||||
final ObservableValue<int> length;
|
||||
PageState()
|
||||
: current = new ObservableValue<int>(0),
|
||||
target = new ObservableValue<int>(0),
|
||||
length = new ObservableValue<int>(1);
|
||||
: current = ObservableValue<int>(0),
|
||||
target = ObservableValue<int>(0),
|
||||
length = ObservableValue<int>(1);
|
||||
}
|
||||
|
||||
/** Simplifies using a PageNumberView and PagedColumnView together. */
|
||||
/// Simplifies using a PageNumberView and PagedColumnView together. */
|
||||
class PagedContentView extends CompositeView {
|
||||
final View content;
|
||||
final PageState pages;
|
||||
|
||||
PagedContentView(this.content)
|
||||
: pages = new PageState(),
|
||||
: pages = PageState(),
|
||||
super('paged-content') {
|
||||
addChild(new PagedColumnView(pages, content));
|
||||
addChild(new PageNumberView(pages));
|
||||
addChild(PagedColumnView(pages, content));
|
||||
addChild(PageNumberView(pages));
|
||||
}
|
||||
}
|
||||
|
||||
/** Displays current page and a left/right arrow. Used with [PagedColumnView] */
|
||||
/// Displays current page and a left/right arrow. Used with [PagedColumnView] */
|
||||
class PageNumberView extends View {
|
||||
final PageState pages;
|
||||
Element _label;
|
||||
|
@ -37,11 +37,12 @@ class PageNumberView extends View {
|
|||
|
||||
PageNumberView(this.pages);
|
||||
|
||||
@override
|
||||
Element render() {
|
||||
// TODO(jmesserly): this was supposed to use the somewhat flatter unicode
|
||||
// glyphs that Chrome uses on the new tab page, but the text is getting
|
||||
// corrupted.
|
||||
final node = new Element.html('''
|
||||
final node = Element.html('''
|
||||
<div class="page-number">
|
||||
<div class="page-number-left">‹</div>
|
||||
<div class="page-number-label"></div>
|
||||
|
@ -54,6 +55,7 @@ class PageNumberView extends View {
|
|||
return node;
|
||||
}
|
||||
|
||||
@override
|
||||
void enterDocument() {
|
||||
watch(pages.current, (s) => _update());
|
||||
watch(pages.length, (s) => _update());
|
||||
|
@ -76,16 +78,14 @@ class PageNumberView extends View {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A horizontal scrolling view that snaps to items like [ConveyorView], but only
|
||||
* has one child. Instead of scrolling between views, it scrolls between content
|
||||
* that flows horizontally in columns. Supports left/right swipe to switch
|
||||
* between pages. Can also be used with [PageNumberView].
|
||||
*
|
||||
* This control assumes that it is styled with fixed or percent width and
|
||||
* height, so the content will flow out horizontally. This allows it to compute
|
||||
* the number of pages using [:scrollWidth:] and [:offsetWidth:].
|
||||
*/
|
||||
/// A horizontal scrolling view that snaps to items like [ConveyorView], but only
|
||||
/// has one child. Instead of scrolling between views, it scrolls between content
|
||||
/// that flows horizontally in columns. Supports left/right swipe to switch
|
||||
/// between pages. Can also be used with [PageNumberView].
|
||||
///
|
||||
/// This control assumes that it is styled with fixed or percent width and
|
||||
/// height, so the content will flow out horizontally. This allows it to compute
|
||||
/// the number of pages using [:scrollWidth:] and [:offsetWidth:].
|
||||
class PagedColumnView extends View {
|
||||
static const MIN_THROW_PAGE_FRACTION = 0.01;
|
||||
final View contentView;
|
||||
|
@ -99,8 +99,9 @@ class PagedColumnView extends View {
|
|||
|
||||
PagedColumnView(this.pages, this.contentView);
|
||||
|
||||
@override
|
||||
Element render() {
|
||||
final node = new Element.html('''
|
||||
final node = Element.html('''
|
||||
<div class="paged-column">
|
||||
<div class="paged-column-container"></div>
|
||||
</div>''');
|
||||
|
@ -113,9 +114,9 @@ class PagedColumnView extends View {
|
|||
// the scroller configured the default way.
|
||||
|
||||
// TODO(jacobr): use named arguments when available.
|
||||
scroller = new Scroller(_container, false /* verticalScrollEnabled */,
|
||||
scroller = Scroller(_container, false /* verticalScrollEnabled */,
|
||||
true /* horizontalScrollEnabled */, true /* momementumEnabled */, () {
|
||||
return new Size(_getViewLength(_container), 1);
|
||||
return Size(_getViewLength(_container), 1);
|
||||
}, Scroller.FAST_SNAP_DECELERATION_FACTOR);
|
||||
|
||||
scroller.onDecelStart.listen(_snapToPage);
|
||||
|
@ -130,6 +131,7 @@ class PagedColumnView extends View {
|
|||
|
||||
// TODO(jmesserly): would be better to not have this code in enterDocument.
|
||||
// But we need computedStyle to read our CSS properties.
|
||||
@override
|
||||
void enterDocument() {
|
||||
scheduleMicrotask(() {
|
||||
var style = contentView.node.getComputedStyle();
|
||||
|
@ -153,7 +155,7 @@ class PagedColumnView extends View {
|
|||
});
|
||||
}
|
||||
|
||||
/** Read the column-gap setting so we know how far to translate the child. */
|
||||
/// Read the column-gap setting so we know how far to translate the child. */
|
||||
void _computeColumnGap(CssStyleDeclaration style) {
|
||||
String gap = style.columnGap;
|
||||
if (gap == 'normal') {
|
||||
|
@ -172,7 +174,8 @@ class PagedColumnView extends View {
|
|||
return double.parse(value).round();
|
||||
}
|
||||
|
||||
/** Watch for resize and update page count. */
|
||||
/// Watch for resize and update page count. */
|
||||
@override
|
||||
void windowResized() {
|
||||
// TODO(jmesserly): verify we aren't triggering unnecessary layouts.
|
||||
|
||||
|
|
|
@ -6,12 +6,10 @@
|
|||
|
||||
part of view;
|
||||
|
||||
typedef void SelectHandler(String menuText);
|
||||
typedef SelectHandler = void Function(String menuText);
|
||||
|
||||
/**
|
||||
* This implements a horizontal menu bar with a sliding triangle arrow
|
||||
* that points at the currently selected item.
|
||||
*/
|
||||
/// This implements a horizontal menu bar with a sliding triangle arrow
|
||||
/// that points at the currently selected item.
|
||||
class SliderMenu extends View {
|
||||
static const int TRIANGLE_WIDTH = 24;
|
||||
|
||||
|
@ -24,25 +22,24 @@ class SliderMenu extends View {
|
|||
// TODO(mattsh) - move this to a touch mixin
|
||||
Element touchItem;
|
||||
|
||||
/**
|
||||
* Callback function that we call when the user chooses something from
|
||||
* the menu. This is passed the menu item text.
|
||||
*/
|
||||
/// Callback function that we call when the user chooses something from
|
||||
/// the menu. This is passed the menu item text.
|
||||
SelectHandler onSelect;
|
||||
|
||||
List<String> _menuItems;
|
||||
final List<String> _menuItems;
|
||||
|
||||
SliderMenu(this._menuItems, this.onSelect);
|
||||
|
||||
@override
|
||||
Element render() {
|
||||
// Create a div for each menu item.
|
||||
final items = new StringBuffer();
|
||||
final items = StringBuffer();
|
||||
for (final item in _menuItems) {
|
||||
items.write('<div class="sm-item">$item</div>');
|
||||
}
|
||||
|
||||
// Create a root node to hold this view.
|
||||
return new Element.html('''
|
||||
return Element.html('''
|
||||
<div class="sm-root">
|
||||
<div class="sm-item-box">
|
||||
<div class="sm-item-filler"></div>
|
||||
|
@ -56,6 +53,7 @@ class SliderMenu extends View {
|
|||
''');
|
||||
}
|
||||
|
||||
@override
|
||||
void enterDocument() {
|
||||
// select the first item
|
||||
// todo(jacobr): too much actual work is performed in enterDocument.
|
||||
|
@ -95,10 +93,8 @@ class SliderMenu extends View {
|
|||
window.onResize.listen((Event event) => updateIndicator(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks the parent chain of the first Touch target to find the first ancestor
|
||||
* that has sm-item class.
|
||||
*/
|
||||
/// Walks the parent chain of the first Touch target to find the first ancestor
|
||||
/// that has sm-item class.
|
||||
Element itemOfTouchEvent(event) {
|
||||
Node node = event.changedTouches[0].target;
|
||||
return itemOfNode(node);
|
||||
|
@ -153,9 +149,7 @@ class SliderMenu extends View {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* animate - if true, then animate the movement of the triangle slider
|
||||
*/
|
||||
/// animate - if true, then animate the movement of the triangle slider
|
||||
void updateIndicator(bool animate) {
|
||||
if (selectedItem != null) {
|
||||
// calculate where we want to put the triangle
|
||||
|
|
|
@ -26,7 +26,7 @@ part 'SliderMenu.dart';
|
|||
// subclasses are refactored to use the new way. There will be some scaffolding
|
||||
// and construction cones laying around. Try not to freak out.
|
||||
|
||||
/** A generic view. */
|
||||
/// A generic view. */
|
||||
class View implements Positionable {
|
||||
Element _node;
|
||||
ViewLayout _layout;
|
||||
|
@ -35,25 +35,25 @@ class View implements Positionable {
|
|||
// App track the views that want to be notified of resize()
|
||||
StreamSubscription _resizeSubscription;
|
||||
|
||||
/**
|
||||
* Style properties configured for this view.
|
||||
*/
|
||||
/// Style properties configured for this view.
|
||||
// TODO(jmesserly): We should be getting these from our CSS preprocessor.
|
||||
// I'm not sure if this will stay as a Map, or just be a get method.
|
||||
// TODO(jacobr): Consider returning a somewhat typed base.Style wrapper
|
||||
// object instead, and integrating with built in CSS properties.
|
||||
@override
|
||||
final Map<String, String> customStyle;
|
||||
|
||||
View() : customStyle = new Map<String, String>();
|
||||
View() : customStyle = <String, String>{};
|
||||
|
||||
View.fromNode(Element this._node) : customStyle = new Map<String, String>();
|
||||
View.fromNode(this._node) : customStyle = <String, String>{};
|
||||
|
||||
View.html(String html)
|
||||
: customStyle = new Map<String, String>(),
|
||||
_node = new Element.html(html);
|
||||
: customStyle = <String, String>{},
|
||||
_node = Element.html(html);
|
||||
|
||||
// TODO(rnystrom): Get rid of this when all views are refactored to not use
|
||||
// it.
|
||||
@override
|
||||
Element get node {
|
||||
// Lazy render.
|
||||
if (_node == null) {
|
||||
|
@ -63,22 +63,19 @@ class View implements Positionable {
|
|||
return _node;
|
||||
}
|
||||
|
||||
/**
|
||||
* A subclass that contains child views should override this to return those
|
||||
* views. View uses this to ensure that child views are properly rendered
|
||||
* and initialized when their parent view is without the parent having to
|
||||
* manually handle that traversal.
|
||||
*/
|
||||
/// A subclass that contains child views should override this to return those
|
||||
/// views. View uses this to ensure that child views are properly rendered
|
||||
/// and initialized when their parent view is without the parent having to
|
||||
/// manually handle that traversal.
|
||||
@override
|
||||
Iterable<View> get childViews {
|
||||
return const [];
|
||||
}
|
||||
|
||||
/**
|
||||
* View presumes the collection of views returned by childViews is more or
|
||||
* less static after the view is first created. Subclasses should call this
|
||||
* when that invariant doesn't hold to let View know that a new childView has
|
||||
* appeared.
|
||||
*/
|
||||
/// View presumes the collection of views returned by childViews is more or
|
||||
/// less static after the view is first created. Subclasses should call this
|
||||
/// when that invariant doesn't hold to let View know that a new childView has
|
||||
/// appeared.
|
||||
void childViewAdded(View child) {
|
||||
if (isInDocument) {
|
||||
child._enterDocument();
|
||||
|
@ -88,37 +85,31 @@ class View implements Positionable {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* View presumes the collection of views returned by childViews is more or
|
||||
* less static after the view is first created. Subclasses should call this
|
||||
* when that invariant doesn't hold to let View know that a childView has
|
||||
* been removed.
|
||||
*/
|
||||
/// View presumes the collection of views returned by childViews is more or
|
||||
/// less static after the view is first created. Subclasses should call this
|
||||
/// when that invariant doesn't hold to let View know that a childView has
|
||||
/// been removed.
|
||||
void childViewRemoved(View child) {
|
||||
if (isInDocument) {
|
||||
child._exitDocument();
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets whether this View has already been rendered or not. */
|
||||
/// Gets whether this View has already been rendered or not. */
|
||||
bool get isRendered {
|
||||
return _node != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether this View (or one of its parents) has been added to the
|
||||
* document or not.
|
||||
*/
|
||||
/// Gets whether this View (or one of its parents) has been added to the
|
||||
/// document or not.
|
||||
bool get isInDocument {
|
||||
return _node != null &&
|
||||
node.ownerDocument is HtmlDocument &&
|
||||
(node.ownerDocument as HtmlDocument).body.contains(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds this view to the document as a child of the given node. This should
|
||||
* generally only be called once for the top-level view.
|
||||
*/
|
||||
/// Adds this view to the document as a child of the given node. This should
|
||||
/// generally only be called once for the top-level view.
|
||||
void addToDocument(Element parentNode) {
|
||||
assert(!isInDocument);
|
||||
|
||||
|
@ -137,66 +128,58 @@ class View implements Positionable {
|
|||
_node.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this to generate the DOM structure for the view.
|
||||
*/
|
||||
/// Override this to generate the DOM structure for the view.
|
||||
// TODO(rnystrom): make this method abstract, see b/5015671
|
||||
Element render() {
|
||||
throw 'abstract';
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this to perform initialization behavior that requires access to
|
||||
* the DOM associated with the View, such as event wiring.
|
||||
*/
|
||||
/// Override this to perform initialization behavior that requires access to
|
||||
/// the DOM associated with the View, such as event wiring.
|
||||
void afterRender(Element node) {
|
||||
// Do nothing by default.
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this to perform behavior after this View has been added to the
|
||||
* document. This is appropriate if you need access to state (such as the
|
||||
* calculated size of an element) that's only available when the View is in
|
||||
* the document.
|
||||
*
|
||||
* This will be called each time the View is added to the document, if it is
|
||||
* added and removed multiple times.
|
||||
*/
|
||||
/// Override this to perform behavior after this View has been added to the
|
||||
/// document. This is appropriate if you need access to state (such as the
|
||||
/// calculated size of an element) that's only available when the View is in
|
||||
/// the document.
|
||||
///
|
||||
/// This will be called each time the View is added to the document, if it is
|
||||
/// added and removed multiple times.
|
||||
void enterDocument() {}
|
||||
|
||||
/**
|
||||
* Override this to perform behavior after this View has been removed from the
|
||||
* document. This can be a convenient time to unregister event handlers bound
|
||||
* in enterDocument().
|
||||
*
|
||||
* This will be called each time the View is removed from the document, if it
|
||||
* is added and removed multiple times.
|
||||
*/
|
||||
/// Override this to perform behavior after this View has been removed from the
|
||||
/// document. This can be a convenient time to unregister event handlers bound
|
||||
/// in enterDocument().
|
||||
///
|
||||
/// This will be called each time the View is removed from the document, if it
|
||||
/// is added and removed multiple times.
|
||||
void exitDocument() {}
|
||||
|
||||
/** Override this to perform behavior after the window is resized. */
|
||||
/// Override this to perform behavior after the window is resized. */
|
||||
// TODO(jmesserly): this isn't really the event we want. Ideally we want to
|
||||
// fire the event only if this particular View changed size. Also we should
|
||||
// give a view the ability to measure itself when added to the document.
|
||||
void windowResized() {}
|
||||
|
||||
/**
|
||||
* Registers the given listener callback to the given observable. Also
|
||||
* immediately invokes the callback once as if a change has just come in.
|
||||
* This lets you define a render() method that renders the skeleton of a
|
||||
* view, then register a bunch of listeners which all fire to populate the
|
||||
* view with model data.
|
||||
*/
|
||||
void watch(Observable observable, void watcher(EventSummary summary)) {
|
||||
/// Registers the given listener callback to the given observable. Also
|
||||
/// immediately invokes the callback once as if a change has just come in.
|
||||
/// This lets you define a render() method that renders the skeleton of a
|
||||
/// view, then register a bunch of listeners which all fire to populate the
|
||||
/// view with model data.
|
||||
void watch(
|
||||
Observable observable, void Function(EventSummary summary) watcher) {
|
||||
// Make a fake summary for the initial watch.
|
||||
final summary = new EventSummary(observable);
|
||||
final summary = EventSummary(observable);
|
||||
watcher(summary);
|
||||
|
||||
attachWatch(observable, watcher);
|
||||
}
|
||||
|
||||
/** Registers the given listener callback to the given observable. */
|
||||
void attachWatch(Observable observable, void watcher(EventSummary summary)) {
|
||||
/// Registers the given listener callback to the given observable. */
|
||||
void attachWatch(
|
||||
Observable observable, void Function(EventSummary summary) watcher) {
|
||||
observable.addChangeListener(watcher);
|
||||
|
||||
// TODO(rnystrom): Should keep track of this and unregister when the view
|
||||
|
@ -207,15 +190,11 @@ class View implements Positionable {
|
|||
_node.onClick.listen(handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether the view is hidden.
|
||||
*/
|
||||
/// Gets whether the view is hidden.
|
||||
bool get hidden => _node.style.display == 'none';
|
||||
|
||||
/**
|
||||
* Sets whether the view is hidden.
|
||||
*/
|
||||
void set hidden(bool hidden) {
|
||||
/// Sets whether the view is hidden.
|
||||
set hidden(bool hidden) {
|
||||
if (hidden) {
|
||||
node.style.display = 'none';
|
||||
} else {
|
||||
|
@ -231,43 +210,35 @@ class View implements Positionable {
|
|||
node.classes.remove(className);
|
||||
}
|
||||
|
||||
/** Sets the CSS3 transform applied to the view. */
|
||||
/// Sets the CSS3 transform applied to the view. */
|
||||
set transform(String transform) {
|
||||
node.style.transform = transform;
|
||||
}
|
||||
|
||||
// TODO(rnystrom): Get rid of this, or move into a separate class?
|
||||
/** Creates a View whose node is a <div> with the given class(es). */
|
||||
static View div(String cssClass, [String body = null]) {
|
||||
if (body == null) {
|
||||
body = '';
|
||||
}
|
||||
return new View.html('<div class="$cssClass">$body</div>');
|
||||
/// Creates a View whose node is a <div> with the given class(es). */
|
||||
static View div(String cssClass, [String body]) {
|
||||
body ??= '';
|
||||
return View.html('<div class="$cssClass">$body</div>');
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal render method that deals with traversing child views. Should not
|
||||
* be overridden.
|
||||
*/
|
||||
/// Internal render method that deals with traversing child views. Should not
|
||||
/// be overridden.
|
||||
void _render() {
|
||||
// TODO(rnystrom): Should render child views here. Not implemented yet.
|
||||
// Instead, we rely on the parent accessing .node to implicitly cause the
|
||||
// child to be rendered.
|
||||
|
||||
// Render this view.
|
||||
if (_node == null) {
|
||||
_node = render();
|
||||
}
|
||||
_node ??= render();
|
||||
|
||||
// Pass the node back to the derived view so it can register event
|
||||
// handlers on it.
|
||||
afterRender(_node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method that deals with traversing child views. Should not be
|
||||
* overridden.
|
||||
*/
|
||||
/// Internal method that deals with traversing child views. Should not be
|
||||
/// overridden.
|
||||
void _enterDocument() {
|
||||
// Notify the children first.
|
||||
for (final child in childViews) {
|
||||
|
@ -279,17 +250,14 @@ class View implements Positionable {
|
|||
|
||||
// Layout related methods
|
||||
|
||||
@override
|
||||
ViewLayout get layout {
|
||||
if (_layout == null) {
|
||||
_layout = new ViewLayout.fromView(this);
|
||||
}
|
||||
_layout ??= ViewLayout.fromView(this);
|
||||
return _layout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method that deals with traversing child views. Should not be
|
||||
* overridden.
|
||||
*/
|
||||
/// Internal method that deals with traversing child views. Should not be
|
||||
/// overridden.
|
||||
void _exitDocument() {
|
||||
// Notify this View first so that it's children are still valid.
|
||||
exitDocument();
|
||||
|
@ -300,11 +268,9 @@ class View implements Positionable {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If needed, starts a layout computation from the top level.
|
||||
* Also hooks the relevant events like window resize, so we can layout on too
|
||||
* demand.
|
||||
*/
|
||||
/// If needed, starts a layout computation from the top level.
|
||||
/// Also hooks the relevant events like window resize, so we can layout on too
|
||||
/// demand.
|
||||
void _hookGlobalLayoutEvents() {
|
||||
if (_resizeSubscription == null) {
|
||||
var handler = EventBatch.wrap((e) => doLayout());
|
||||
|
@ -322,6 +288,7 @@ class View implements Positionable {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void doLayout() {
|
||||
_measureLayout().then((changed) {
|
||||
if (changed) {
|
||||
|
@ -332,7 +299,7 @@ class View implements Positionable {
|
|||
|
||||
Future<bool> _measureLayout() {
|
||||
// TODO(10459): code should not use Completer.sync.
|
||||
final changed = new Completer<bool>.sync();
|
||||
final changed = Completer<bool>.sync();
|
||||
_measureLayoutHelper(changed);
|
||||
|
||||
var changedComplete = false;
|
||||
|
@ -356,10 +323,9 @@ class View implements Positionable {
|
|||
// a good tradeoff?
|
||||
if (ViewLayout.hasCustomLayout(this)) {
|
||||
// TODO(10459): code should not use Completer.sync.
|
||||
Completer sizeCompleter = new Completer<Size>.sync();
|
||||
Completer sizeCompleter = Completer<Size>.sync();
|
||||
scheduleMicrotask(() {
|
||||
sizeCompleter
|
||||
.complete(new Size(_node.client.width, _node.client.height));
|
||||
sizeCompleter.complete(Size(_node.client.width, _node.client.height));
|
||||
});
|
||||
layout.measureLayout(sizeCompleter.future, changed);
|
||||
} else {
|
||||
|
|
Loading…
Reference in a new issue