Scrollable Card Stack - flutter

I want to create a scrollable card stack. I want the back one to disappear and the front one to disappear when I slide what is seen in the photo, how can I do that?

The below code should work.
On tapping/clicking anywhere the cards should change position with the topmost card going to the back.
Changing _spacing's value changes the relative horizontal and vertical gaps among the cards in the stack.
Note that the members of the _colors list are in reverse order. This is because, with Stack, widgets are displayed on top of each other based on the last widgets that were added to the children list. This also explains why the call back to setState makes the last member of the _colors list to become the first member instead.
class StackSwipe extends StatefulWidget {
const StackSwipe({Key? key}) : super(key: key);
#override
State<StackSwipe> createState() => _StackSwipeState();
}
class _StackSwipeState extends State<StackSwipe> {
final double _spacing = 16.0;
late double _baseWidth;
final _colors = [
Colors.greenAccent.shade100,
Colors.pink.shade100,
Colors.amber.shade100,
];
#override
Widget build(BuildContext context) {
var deviceWidth = MediaQuery.of(context).size.width;
_baseWidth = deviceWidth > 544 ? 512.0 : deviceWidth - 32;
return Padding(
padding: const EdgeInsets.all(16),
child: GestureDetector(
child: Stack(children: [
..._colors.asMap().entries.map(
(entry) => Positioned(
top: _spacing * entry.key,
left: _spacing * (_colors.length - (entry.key + 1)),
child: Container(
height: 200,
width: _baseWidth -
(_spacing * (_colors.length - (entry.key + 1)) * 2),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: entry.value,
),
),
),
)
]),
onTap: () => setState(() => _colors.insert(0, _colors.removeLast())),
),
);
}
}

Related

Create Dynamically Sized Squares

I'm attempting to create a GitHub style heat map inside a Card and am struggling with the UI. The challenge is making the heat map dynamically expand to fit the Card it sits in based on the device's screen size.
Here is an example screenshot.
The code to create the screenshot is below.
Essentially the code,
creates a column that starts with two lines of text
then inserts a Row of Columns that consist of squares
I'm not sure if I should focus on making the individual boxes expand, the columns that the individual boxes sit in, or both. All my experiments end in unbound errors. I'm not sure where/how to add the constraints.
I also assume I'll need the boxes to be wrapped in AspectRatio() to keep the 1:1 ratio and be a square.
(I've removed some of the the more verbose business logic in my actual code for simplicity.)
class ProfileView extends StatelessWidget {
const ProfileView({Key? key}) : super(key: key);
List<Widget> _heatMapColumnList() {
final _columns = <Widget>[];
final _startDate = DateTime.now().subtract(const Duration(days: 365));
final _endDate = DateTime.now();
final _dateDifference = _endDate.difference(_startDate).inDays;
for (var index = 0 - (_startDate.weekday % 7);
index <= _endDate.difference(_startDate).inDays;
index += 7) {
//helper to change date by index
final _firstDay = DateUtility.changeDay(_startDate, index);
_columns.add(
HeatMapColumn(
startDate: _firstDay,
endDate: index <= _dateDifference - 7
? DateUtility.changeDay(_startDate, index + 6)
: _endDate,
numDays: min(_endDate.difference(_firstDay).inDays + 1, 7),
),
);
}
return _columns;
}
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Card(
elevation: 1,
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Text('Some Title Text'),
const Text('More SubTitle Text'),
const SizedBox(height: 10),
Row(
children: <Widget>[
..._heatMapColumnList(),
],
...
...
class HeatMapColumn extends StatelessWidget {
HeatMapColumn({
super.key,
required this.startDate,
required this.endDate,
required this.numDays,
}) : dayContainers = List.generate(
numDays,
(i) => HeatMapBox(
date: DateUtility.changeDay(startDate, 1),
),
),
emptySpace = (numDays != 7)
? List.generate(
7 - numDays,
(i) => const HeatMapBox(
date: null,
),
)
: [];
final List<Widget> dayContainers;
final List<Widget> emptySpace;
final DateTime startDate;
final DateTime endDate;
final int numDays;
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
children: <Widget>[
...dayContainers,
...emptySpace,
],
...
// !!!THIS IS THE BOX I WANT TO DYNAMICALLY RESIZE!!!
class HeatMapBox extends StatelessWidget {
const HeatMapBox({
required this.date,
this.color,
super.key,
});
final DateTime? date;
final Color? color;
#override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(1),
child: SizedBox(
child: Container(
// ???HOW DO I AVOID THIS EXPLICIT NUMERIC CONTAINER SIZE???
height: 3,
width: 3,
decoration: const BoxDecoration(
color: Colors.black12,
),
),
),
);
}
}
I would add a comment but I do not have enough reputation so sorry if this is not the answer you are looking for
You could use something like this
double width = MediaQuery.of(context).size.width; // gives width of device screen
double height = MediaQuery.of(context).size.height; // gives height of device screen
// if the card has padding
double cardLeftPadding = a double;
double cardRightPadding = a double;
width -= (cardLeftPadding + cardRightPadding);
Container(
// ???HOW DO I AVOID THIS EXPLICIT NUMERIC CONTAINER SIZE???
height: 3,
width: width,
decoration: const BoxDecoration(
color: Colors.black12,
),),
I believe something like this will allow you to fit your heat map to the full length of your card

How to communicated between sibling in Flutter?

So I have 3 tabs HEX , RGB, and HSL as StatefulWidget.
I have these 3 tabs in a Row in a StatelessWidget.
By default, HEX is selected, which I achieved successfully by initialising initState(). What I want to achieve now is that if I tap on any tab, the other two have to automatically go to a deselected state.
I used GestureDetector but it only changes the state of the tab I tapped on. I toggled the styles of all 3 tabs in the setState() function, but I guess the changes don't take place as the other 2 tabs are not built again, only the tab I tapped on is built again. By built I mean createState().
I am a beginner in Flutter, and I have spent a whole day finding solutions to this but to no avail.
I tried rebuilding the parent Row but I am not able to do so. I am trying to find a method by which I can rebuild widgets by key.
Any solution?
EDIT: The code for that section ->
key arrayKey = GlobalKey();
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//++++++++++++++++++++CHIPS++++++++++++++++++++++++++++++++++++++++++++++++
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
class chipMaker extends StatefulWidget {
chipMaker({required this.label}) : key = ObjectKey(label), chipSelect = false;
final String label;
final Key key;
bool chipSelect;
#override
_chipState createState() => _chipState();
}
class _chipState extends State<chipMaker> {
chipMaker get chip => super.widget;
//need to add setState();
#override
Widget build(BuildContext context) {
// TODO: implement build
return GestureDetector(
onTap: () {
//function
},
child: Container(
margin: EdgeInsets.only(left: 8),
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
border: Border.all(
color: defaultColor,
width: 2,
style: BorderStyle.solid,
),
borderRadius: BorderRadius.all(Radius.circular(4)),
color: chip.chipSelect ? defaultColor : Colors.white,
),
child: Text(
chip.label,
style: TextStyle(
fontSize: 10,
height: 1.2,
color: chip.chipSelect ? Colors.white : defaultColor,
),
),
),
);
}
}
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//++++++++++++++++++++ARRAY++++++++++++++++++++++++++++++++++++++++++++++++
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
class chipArray extends StatelessWidget {
chipArray({Key? arrayKey,}) : super(key: arrayKey);
#override
Widget build(BuildContext context) {
// TODO: implement build
return Row(
children: [
chipMaker(label: 'HEX',),
chipMaker(label: 'RGB',),
chipMaker(label: 'HSL',),
],
);
}
}

RangeError (index): Invalid value: Valid value range is empty: 1

after whole day of trying to solve this myself I had to come and ask for help.
I'm trying to build this ListView.builder, it has fixed amount of itemCount. And its building Widgets using data retrieved from locally stored JSON file.
I'm using Provider to pass that data around. The problem is, on app start or hot restart that ListView.builder turns red and shows error, and then after like quarter of a second it shows my data.
I understand why this happens, my list of data that I get from json is initially empty. So I put ternary operator like: provider.data == null ? CircularProgressIndicator() : ListView.builder... but this doesnt stop it from crashing.
I dont know why and its driving me crazy. Here is full code:
We are talking here about widget called RecommendedCardList, its showing widgets from above mentioned list by having random number (in range of list length) as index.
I have similar ListView on HomeScreen called CategoryCardList and its working similarly to RecommendedCardList but I'm not having this issue with it. Also the rest of the home screen shows good, only the portion where RecommendedCardList is turns red for a short period of time.
Home Screen class:
class HomeScreen extends StatelessWidget {
#override
Widget build(BuildContext context) {
// Get user's screen properties
// We are using this properties to adjust size accordingly
// In order to achieve maximum responsivnes for different screen sizes
var height = MediaQuery.of(context).size.height;
var width = MediaQuery.of(context).size.width;
var repoProvider = Provider.of<Repository>(context);
var recipeDataList = repoProvider.recipeDataList;
return Container(
color: backgroundColor,
child: repoProvider.recipeDataList == null
? Center(child: CircularProgressIndicator())
: Padding(
padding: contentPadding,
child: ListView(
children: <Widget>[
AppTitle(),
SizedBox(
height: height * 0.03,
),
Column(
children: <Widget>[
CategoryAndSeeAll(),
CategoryCardsList(height: height, provider: repoProvider),
],
),
SizedBox(
height: height * 0.05,
),
Container(
width: double.infinity,
height: height * 0.1,
decoration: BoxDecoration(
border: Border.all(color: accentColor),
),
child: Text(
'Reserved for AD',
textAlign: TextAlign.center,
),
),
SizedBox(
height: height * 0.05,
),
RecommendedCardsList(height: height, width: width, recipeDataList: recipeDataList),
],
),
),
);
}
}
RecommendedCardsList class:
class RecommendedCardsList extends StatelessWidget {
const RecommendedCardsList({
Key key,
#required this.height,
#required this.width,
#required this.recipeDataList,
}) : super(key: key);
final double height;
final double width;
final recipeDataList;
#override
Widget build(BuildContext context) {
return Container(
height: height * 0.30,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: numberOfRecommendedRecipes,
itemBuilder: (context, counter) {
int randomNumber = Random().nextInt(recipeDataList.length);
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
RecommendedCard(
width: width,
height: height,
imagePath: recipeDataList.elementAt(randomNumber).image,
text: recipeDataList.elementAt(randomNumber).title,
),
],
);
}),
);
}
}
Repository class:
class Repository extends ChangeNotifier {
Repository() {
loadJson();
}
var _recipeData;
List<RecipeModel> _recipeDataList = [];
List<RecipeModel> get recipeDataList => _recipeDataList;
void loadJson() async {
var json = await rootBundle.loadString('assets/recipes.json');
var parsedJson = jsonDecode(json);
for (var item in parsedJson) {
_recipeData = RecipeModel.fromJson(item);
_recipeDataList.add(_recipeData);
}
//print('Title:${_recipeDataList[0].title}\nImage:${_recipeDataList[0].image}'); // For debugging
notifyListeners();
}
}
This error is related to the fact that the code searched for an index in your list and this index is more than you list length.
I think the error is in that part:
int randomNumber = Random().nextInt(recipeDataList.length);
Supposing the length is 10 the random function will retrieve a num between 0 and 10, but the last index is 9.
With that in mind, I have two suggestions:
1)
// changing ternary logic
(repoProvider.recipeDataList == null && repoProvider.recipeDataList.length > 0)
2)
// inside ListView.Builder change to get the list length
itemCount: recipeDataList.length
Put the following condition in build() of RecommendedCardsList widget as the first line.
if(recipeDataList == null || recipeDataList.length == 0){
return Container();
}

Add indicator to BottomNavigationBar in flutter

any way to add indicator to BottomNavigatorBarItem like this image?
This package should be able to help you achieve it.
You can use a TabBar instead of a BottomNavigationBar using a custom decoration:
class TopIndicator extends Decoration {
#override
BoxPainter createBoxPainter([VoidCallback? onChanged]) {
return _TopIndicatorBox();
}
}
class _TopIndicatorBox extends BoxPainter {
#override
void paint(Canvas canvas, Offset offset, ImageConfiguration cfg) {
Paint _paint = Paint()
..color = Colors.lightblue
..strokeWidth = 5
..isAntiAlias = true;
canvas.drawLine(offset, Offset(cfg.size!.width + offset.dx, 0), _paint);
}
}
Then pass the decoration to the TapBar using TapBar(indicator: TopIndicator ...).
To use the TabBar as the Scaffold.bottomNavigationBar, you will most likely want to wrap it in a Material to apply a background color:
Scaffold(
bottomNavigationBar: Material(
color: Colors.white,
child: TabBar(
indicator: TopIndicator(),
tabs: const <Widget>[
Tab(icon: Icon(Icons.home_outlined), text: 'Reward'),
...
],
),
),
...
)
Thanks Ara Kurghinyan for the original idea.
I've had the same problem and all the packages I found seem to require raw IconData, which makes it impossible to use widget functionality like number badges (e.g. the number of unread chat messages).
I came up with my own little solution; first, I made a widget to display the actual indicators:
class TabIndicators extends StatelessWidget {
final int _numTabs;
final int _activeIdx;
final Color _activeColor;
final Color _inactiveColor;
final double _padding;
final double _height;
const TabIndicators({
required int numTabs,
required int activeIdx,
required Color activeColor,
required double padding,
required double height,
Color inactiveColor = const Color(0x00FFFFFF),
Key? key }) :
_numTabs = numTabs,
_activeIdx = activeIdx,
_activeColor = activeColor,
_inactiveColor = inactiveColor,
_padding = padding,
_height = height,
super(key: key);
#override
Widget build(BuildContext context) {
final elements = <Widget>[];
for(var i = 0; i < _numTabs; ++i) {
elements.add(
Expanded(child:
Padding(
padding: EdgeInsets.symmetric(horizontal: _padding),
child: Container(color: i == _activeIdx ? _activeColor : _inactiveColor),
)
)
);
}
return
SizedBox(
height: _height,
child: Row(
mainAxisSize: MainAxisSize.max,
children: elements,
),
);
}
}
This can be prepended to the actual BottomNavigationBar like this:
bottomNavigationBuilder: (context, tabsRouter) {
return Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TabIndicators(
activeIdx: tabsRouter.activeIndex,
activeColor: Theme.of(context).primaryColor,
numTabs: 4,
padding: 25,
height: 4,
),
BottomNavigationBar(...
This works perfectly well for me, but to make it look decent, you'd have to set the BottomNavigationBar's elevation to zero, otherwise there's still a faint horizontal line between the indicators and the icons.

Text label that explains what a certain button in the app would do

I need to create a text label component that will act as an in-app tutorial explanatory label in Flutter. This text label should show up above a button explaining the clicking on that button would result in creating a new entry.
I tried using a Tooltip widget to wrap the button. But tooltip shows up only when long pressed the button. I need this widget to be visible if the user doesn't already have any entries created.
Currently, I thought of giving you this short but small help about this. I just want you to see whether it is helping you in any way or not.
My idea is to trigger the tooltip programatically. Let me know tell you how to do that, although there is no available documentation to do that. I did some research too on this.
Use ensureTooltipVisible from _TooltipState using a GlobalKey to fetch it.
Typically you'd do the following field inside the widget instantiating Tooltip : final key = new GlobalKey();
Then in your tooltip you will assign this key
final _key = new GlobalKey()
ToolTip(
key: _key
)
And in the initState() you just check whether there are some entries or not:
initState(){
super.initState();
if(_yourData == null){
//that's how you trigger it
final dynamic tooltip = _key.currentState;
tooltip.ensureTooltipVisible();
}
}
initState() is the method which gets initialized when you open up your page every time. So as soon as you will open up your page, the info will be checked and hence it will show accordingly. Let me know if that helps. :)
Can make using OverlayEntry.
Here is sample code
final overlayEntry = OverlayEntry(
builder: (BuildContext context) => Positioned(
top: _yourCustomTop,
left: _yourCustomLeft,
child: FadeTransition(
opacity: CurvedAnimation(
parent: _yourController,
curve: Curves.fastOutSlowIn,
),
child: Material(
color: const Color(0x00ffffff),
child: Container(
decoration: ShapeDecoration(
color: Colors.white,
shape: TooltipShapeBorder(radius: 6, arrowArc: 0.2),
shadows: [
const BoxShadow(
color: Colors.black26,
blurRadius: 4.0,
offset: const Offset(2, 2),
),
],
),
child: SizedBox(
width: 84,
height: 44,
child: Center(
child: Text('_yourString'),
),
),
),
);
),
),
);
Overlay.of(context).insert(_overlayEntry);
TooltipShapeBorder
from here.
import 'package:flutter/material.dart';
class TooltipShapeBorder extends ShapeBorder {
final double arrowWidth;
final double arrowHeight;
final double arrowArc;
final double radius;
TooltipShapeBorder({
this.radius = 16.0,
this.arrowWidth = 20.0,
this.arrowHeight = 10.0,
this.arrowArc = 0.0,
}) : assert(arrowArc <= 1.0 && arrowArc >= 0.0);
#override
EdgeInsetsGeometry get dimensions => EdgeInsets.only(bottom: arrowHeight);
#override
Path getInnerPath(Rect rect, {TextDirection textDirection}) => null;
#override
Path getOuterPath(Rect rect, {TextDirection textDirection}) {
rect = Rect.fromPoints(rect.topLeft, rect.bottomRight - Offset(0, arrowHeight));
double x = arrowWidth, y = arrowHeight, r = 1 - arrowArc;
return Path()
..addRRect(RRect.fromRectAndRadius(rect, Radius.circular(radius)))
..moveTo(rect.bottomCenter.dx + x / 2, rect.bottomCenter.dy)
..relativeLineTo(-x / 2 * r, y * r)
..relativeQuadraticBezierTo(-x / 2 * (1 - r), y * (1 - r), -x * (1 - r), 0)
..relativeLineTo(-x / 2 * r, -y * r);
}
#override
void paint(Canvas canvas, Rect rect, {TextDirection textDirection}) {}
#override
ShapeBorder scale(double t) => this;
}
You can already show a label quite easily like this:
const Tooltip(
message: 'Hi there!',
triggerMode: TooltipTriggerMode.tap, // ensures the label appears when tapped
preferBelow: false, // use this if you want the label above the widget
child: Icon(Icons.interests),
)
You can use triggerMode: TooltipTriggerMode.manual for further customization ;)
Or you could use the showcaseview package
You can find more info for the Tooltip widget here