mirror of
https://github.com/flutter/samples.git
synced 2026-05-28 01:49:24 +00:00
Moving Shrine into a material_studies subdirectory (#128)
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
// Copyright 2018-present the Flutter authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../model/product.dart';
|
||||
import 'product_columns.dart';
|
||||
|
||||
class AsymmetricView extends StatelessWidget {
|
||||
final List<Product> products;
|
||||
|
||||
const AsymmetricView({Key key, this.products});
|
||||
|
||||
List<Container> _buildColumns(BuildContext context) {
|
||||
if (products == null || products.isEmpty) {
|
||||
return const <Container>[];
|
||||
}
|
||||
|
||||
/// This will return a list of columns. It will oscillate between the two
|
||||
/// kinds of columns. Even cases of the index (0, 2, 4, etc) will be
|
||||
/// TwoProductCardColumn and the odd cases will be OneProductCardColumn.
|
||||
///
|
||||
/// Each pair of columns will advance us 3 products forward (2 + 1). That's
|
||||
/// some kinda awkward math so we use _evenCasesIndex and _oddCasesIndex as
|
||||
/// helpers for creating the index of the product list that will correspond
|
||||
/// to the index of the list of columns.
|
||||
return List.generate(_listItemCount(products.length), (index) {
|
||||
double width = .59 * MediaQuery.of(context).size.width;
|
||||
Widget column;
|
||||
if (index % 2 == 0) {
|
||||
/// Even cases
|
||||
int bottom = _evenCasesIndex(index);
|
||||
column = TwoProductCardColumn(
|
||||
bottom: products[bottom],
|
||||
top: products.length - 1 >= bottom + 1
|
||||
? products[bottom + 1]
|
||||
: null);
|
||||
width += 32.0;
|
||||
} else {
|
||||
/// Odd cases
|
||||
column = OneProductCardColumn(
|
||||
product: products[_oddCasesIndex(index)],
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
width: width,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: column,
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
int _evenCasesIndex(int input) {
|
||||
/// The operator ~/ is a cool one. It's the truncating division operator. It
|
||||
/// divides the number and if there's a remainder / decimal, it cuts it off.
|
||||
/// This is like dividing and then casting the result to int. Also, it's
|
||||
/// functionally equivalent to floor() in this case.
|
||||
return input ~/ 2 * 3;
|
||||
}
|
||||
|
||||
int _oddCasesIndex(int input) {
|
||||
assert(input > 0);
|
||||
return (input / 2).ceil() * 3 - 1;
|
||||
}
|
||||
|
||||
int _listItemCount(int totalItems) {
|
||||
if (totalItems % 3 == 0) {
|
||||
return totalItems ~/ 3 * 2;
|
||||
} else {
|
||||
return (totalItems / 3).ceil() * 2 - 1;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: EdgeInsets.fromLTRB(0.0, 34.0, 16.0, 44.0),
|
||||
children: _buildColumns(context),
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
);
|
||||
}
|
||||
}
|
||||
139
material_studies/shrine/lib/supplemental/cut_corners_border.dart
Normal file
139
material_studies/shrine/lib/supplemental/cut_corners_border.dart
Normal file
@@ -0,0 +1,139 @@
|
||||
// Copyright 2018-present the Flutter authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'dart:ui' show lerpDouble;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class CutCornersBorder extends OutlineInputBorder {
|
||||
const CutCornersBorder({
|
||||
BorderSide borderSide = const BorderSide(),
|
||||
BorderRadius borderRadius = const BorderRadius.all(Radius.circular(2.0)),
|
||||
this.cut = 7.0,
|
||||
double gapPadding = 2.0,
|
||||
}) : super(
|
||||
borderSide: borderSide,
|
||||
borderRadius: borderRadius,
|
||||
gapPadding: gapPadding);
|
||||
|
||||
@override
|
||||
CutCornersBorder copyWith({
|
||||
BorderSide borderSide,
|
||||
BorderRadius borderRadius,
|
||||
double gapPadding,
|
||||
double cut,
|
||||
}) {
|
||||
return CutCornersBorder(
|
||||
borderRadius: borderRadius ?? this.borderRadius,
|
||||
borderSide: borderSide ?? this.borderSide,
|
||||
cut: cut ?? this.cut,
|
||||
gapPadding: gapPadding ?? this.gapPadding,
|
||||
);
|
||||
}
|
||||
|
||||
final double cut;
|
||||
|
||||
@override
|
||||
ShapeBorder lerpFrom(ShapeBorder a, double t) {
|
||||
if (a is CutCornersBorder) {
|
||||
final CutCornersBorder outline = a;
|
||||
return CutCornersBorder(
|
||||
borderRadius: BorderRadius.lerp(outline.borderRadius, borderRadius, t),
|
||||
borderSide: BorderSide.lerp(outline.borderSide, borderSide, t),
|
||||
cut: cut,
|
||||
gapPadding: outline.gapPadding,
|
||||
);
|
||||
}
|
||||
return super.lerpFrom(a, t);
|
||||
}
|
||||
|
||||
@override
|
||||
ShapeBorder lerpTo(ShapeBorder b, double t) {
|
||||
if (b is CutCornersBorder) {
|
||||
final CutCornersBorder outline = b;
|
||||
return CutCornersBorder(
|
||||
borderRadius: BorderRadius.lerp(borderRadius, outline.borderRadius, t),
|
||||
borderSide: BorderSide.lerp(borderSide, outline.borderSide, t),
|
||||
cut: cut,
|
||||
gapPadding: outline.gapPadding,
|
||||
);
|
||||
}
|
||||
return super.lerpTo(b, t);
|
||||
}
|
||||
|
||||
Path _notchedCornerPath(Rect center,
|
||||
[double start = 0.0, double extent = 0.0]) {
|
||||
final Path path = Path();
|
||||
if (start > 0.0 || extent > 0.0) {
|
||||
path.relativeMoveTo(extent + start, center.top);
|
||||
_notchedSidesAndBottom(center, path);
|
||||
path..lineTo(center.left + cut, center.top)..lineTo(start, center.top);
|
||||
} else {
|
||||
path.moveTo(center.left + cut, center.top);
|
||||
_notchedSidesAndBottom(center, path);
|
||||
path.lineTo(center.left + cut, center.top);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
Path _notchedSidesAndBottom(Rect center, Path path) {
|
||||
return path
|
||||
..lineTo(center.right - cut, center.top)
|
||||
..lineTo(center.right, center.top + cut)
|
||||
..lineTo(center.right, center.top + center.height - cut)
|
||||
..lineTo(center.right - cut, center.top + center.height)
|
||||
..lineTo(center.left + cut, center.top + center.height)
|
||||
..lineTo(center.left, center.top + center.height - cut)
|
||||
..lineTo(center.left, center.top + cut);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(
|
||||
Canvas canvas,
|
||||
Rect rect, {
|
||||
double gapStart,
|
||||
double gapExtent = 0.0,
|
||||
double gapPercentage = 0.0,
|
||||
TextDirection textDirection,
|
||||
}) {
|
||||
assert(gapExtent != null);
|
||||
assert(gapPercentage >= 0.0 && gapPercentage <= 1.0);
|
||||
|
||||
final Paint paint = borderSide.toPaint();
|
||||
final RRect outer = borderRadius.toRRect(rect);
|
||||
if (gapStart == null || gapExtent <= 0.0 || gapPercentage == 0.0) {
|
||||
canvas.drawPath(_notchedCornerPath(outer.middleRect), paint);
|
||||
} else {
|
||||
final double extent =
|
||||
lerpDouble(0.0, gapExtent + gapPadding * 2.0, gapPercentage);
|
||||
switch (textDirection) {
|
||||
case TextDirection.rtl:
|
||||
{
|
||||
final Path path = _notchedCornerPath(
|
||||
outer.middleRect, gapStart + gapPadding - extent, extent);
|
||||
canvas.drawPath(path, paint);
|
||||
break;
|
||||
}
|
||||
case TextDirection.ltr:
|
||||
{
|
||||
final Path path = _notchedCornerPath(
|
||||
outer.middleRect, gapStart - gapPadding, extent);
|
||||
canvas.drawPath(path, paint);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
93
material_studies/shrine/lib/supplemental/product_card.dart
Normal file
93
material_studies/shrine/lib/supplemental/product_card.dart
Normal file
@@ -0,0 +1,93 @@
|
||||
// Copyright 2018-present the Flutter authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:scoped_model/scoped_model.dart';
|
||||
|
||||
import '../model/app_state_model.dart';
|
||||
import '../model/product.dart';
|
||||
|
||||
class ProductCard extends StatelessWidget {
|
||||
ProductCard({this.imageAspectRatio = 33 / 49, this.product})
|
||||
: assert(imageAspectRatio == null || imageAspectRatio > 0);
|
||||
|
||||
final double imageAspectRatio;
|
||||
final Product product;
|
||||
|
||||
static final kTextBoxHeight = 65.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final NumberFormat formatter = NumberFormat.simpleCurrency(
|
||||
decimalDigits: 0, locale: Localizations.localeOf(context).toString());
|
||||
final ThemeData theme = Theme.of(context);
|
||||
|
||||
final imageWidget = Image.asset(
|
||||
product.assetName,
|
||||
package: product.assetPackage,
|
||||
fit: BoxFit.cover,
|
||||
);
|
||||
|
||||
return ScopedModelDescendant<AppStateModel>(
|
||||
builder: (context, child, model) => GestureDetector(
|
||||
onTap: () {
|
||||
model.addProductToCart(product.id);
|
||||
// TODO: Add Snackbar
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
AspectRatio(
|
||||
aspectRatio: imageAspectRatio,
|
||||
child: imageWidget,
|
||||
),
|
||||
SizedBox(
|
||||
height: kTextBoxHeight * MediaQuery.of(context).textScaleFactor,
|
||||
width: 121.0,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
product == null ? '' : product.name,
|
||||
style: theme.textTheme.button,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
SizedBox(height: 4.0),
|
||||
Text(
|
||||
product == null ? '' : formatter.format(product.price),
|
||||
style: theme.textTheme.caption,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Icon(Icons.add_shopping_cart),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright 2018-present the Flutter authors. All Rights Reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../model/product.dart';
|
||||
import 'product_card.dart';
|
||||
|
||||
class TwoProductCardColumn extends StatelessWidget {
|
||||
TwoProductCardColumn({
|
||||
this.bottom,
|
||||
this.top,
|
||||
}) : assert(bottom != null);
|
||||
|
||||
final Product bottom, top;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
const spacerHeight = 44.0;
|
||||
|
||||
double heightOfCards =
|
||||
(constraints.biggest.height - spacerHeight) / 2.0;
|
||||
double heightOfImages = heightOfCards - ProductCard.kTextBoxHeight;
|
||||
double imageAspectRatio = (heightOfImages >= 0.0 &&
|
||||
constraints.biggest.width > heightOfImages)
|
||||
? constraints.biggest.width / heightOfImages
|
||||
: 33 / 49;
|
||||
|
||||
return ListView(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.only(start: 28.0),
|
||||
child: top != null
|
||||
? ProductCard(
|
||||
imageAspectRatio: imageAspectRatio,
|
||||
product: top,
|
||||
)
|
||||
: SizedBox(
|
||||
height: heightOfCards > 0 ? heightOfCards : spacerHeight,
|
||||
),
|
||||
),
|
||||
SizedBox(height: spacerHeight),
|
||||
Padding(
|
||||
padding: EdgeInsetsDirectional.only(end: 28.0),
|
||||
child: ProductCard(
|
||||
imageAspectRatio: imageAspectRatio,
|
||||
product: bottom,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class OneProductCardColumn extends StatelessWidget {
|
||||
OneProductCardColumn({this.product});
|
||||
|
||||
final Product product;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListView(
|
||||
reverse: true,
|
||||
children: <Widget>[
|
||||
SizedBox(
|
||||
height: 40.0,
|
||||
),
|
||||
ProductCard(
|
||||
product: product,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user