Reputation: 191
I am trying to clone the stock android calculator app. I am not able to figure out how to implement the pullable drawer which opens on the right side.
Here is a gif which shows what I am talking about: https://i.sstatic.net/Db8HM.jpg
Upvotes: 8
Views: 7748
Reputation: 359
You can use a side sheet from Material design. Here you have a package to implement this. You can see the code of this package here
Upvotes: 1
Reputation: 16225
Flutter use native keyboards by default, and what you want to do is make your own custom keyboard.
What you need to do:
I've made a simple example.
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
body: SafeArea(
child: MyStatefulWidget(),
),
),
);
}
}
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({Key key}) : super(key: key);
@override
_MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
TextEditingController _controller;
NoKeyboardEditableTextFocusNode focusNode;
bool isKeyboardOpen = false;
void initState() {
super.initState();
focusNode = NoKeyboardEditableTextFocusNode();
focusNode.addListener(() {
setState(() {
isKeyboardOpen = focusNode.hasFocus;
});
});
_controller = TextEditingController(text: 'tap here');
}
void dispose() {
_controller.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
body: LayoutBuilder(
builder: (context, constraintes) {
var maxHeight = constraintes.maxHeight;
return Column(
children: [
AnimatedContainer(
height: isKeyboardOpen ? maxHeight - 300 : maxHeight,
duration: Duration(milliseconds: 300),
child: Center(
child: GestureDetector(
onTap: () {
setState(() {
isKeyboardOpen = true;
});
},
child: NoKeyboardEditableText(
noKeyboardEditableTextFocusNode: focusNode,
controller: _controller,
cursorColor: Colors.green,
selectionColor: Colors.red,
style: TextStyle(
fontStyle: FontStyle.normal,
fontSize: 30.0,
color: Colors.black),
),
),
),
),
AnimatedContainer(
height: isKeyboardOpen ? 300 : 0,
duration: Duration(milliseconds: 300),
color: Colors.red,
child: _CustomKeybord(
onAdd: (v) => _controller.value =
TextEditingValue(text: _controller.value.text + v)),
),
],
);
},
),
);
}
}
class _CustomKeybord extends StatefulWidget {
_CustomKeybord({Key key, this.onAdd}) : super(key: key);
final Function(String value) onAdd;
@override
__CustomKeybordState createState() => __CustomKeybordState();
}
class __CustomKeybordState extends State<_CustomKeybord> {
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
return Stack(
children: [
Positioned(
left: 0,
top: 0,
bottom: 0,
child: Container(
width: constraints.maxWidth * 0.9,
height: constraints.maxHeight,
child: _FirstLayerKeybord(
onAdd: widget.onAdd,
),
),
),
Positioned(
right: 0,
top: 0,
bottom: 0,
child: _SecondLayerKeybord(
onAdd: widget.onAdd,
),
),
],
);
},
);
}
}
class _SecondLayerKeybord extends StatefulWidget {
const _SecondLayerKeybord({
Key key,
@required this.onAdd,
}) : super(key: key);
final Function(String value) onAdd;
@override
__SecondLayerKeybordState createState() => __SecondLayerKeybordState();
}
class __SecondLayerKeybordState extends State<_SecondLayerKeybord>
with TickerProviderStateMixin {
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
)..addListener(_listener);
}
void _listener() {
if (!_controller.isAnimating) {
setState(() {
isOpen = _controller.isCompleted && _controller.value == 1;
});
}
}
void onTap() {
_controller.isCompleted ? _controller.reverse() : _controller.forward();
}
bool isOpen = false;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
double _currentX;
_onDrag(details) {
var maxWidth = MediaQuery.of(context).size.width;
var x = details.globalPosition.dx;
_currentX = x;
var v = math.max(0.0, 1 - (_currentX / maxWidth - 0.5) * 2);
_controller.value = v;
}
_onDragEnd(_) {
if (_controller.value > .5) {
_controller.animateTo(1);
} else {
_controller.animateTo(0);
}
}
@override
Widget build(BuildContext context) {
var maxWidth = MediaQuery.of(context).size.width;
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Container(
width: maxWidth,
child: Stack(
children: [
Positioned(
left: 0,
right: 0,
top: 0,
bottom: 0,
child: IgnorePointer(
child: Opacity(
opacity: 0.3 * _controller.value,
child: Container(
color: Colors.black,
),
),
),
),
Positioned(
left: maxWidth * 0.9 - (_controller.value * maxWidth * 0.45),
bottom: 0,
top: 0,
child: Container(
decoration: BoxDecoration(
border: Border.all(
color: Colors.blueAccent,
),
),
width: maxWidth * 0.6,
child: Row(
mainAxisSize: MainAxisSize.max,
children: [
GestureDetector(
onTap: onTap,
onPanDown: (details) {
_currentX = details.globalPosition.dx;
},
onPanStart: _onDrag,
onPanUpdate: _onDrag,
onPanEnd: _onDragEnd,
child: Container(
alignment: Alignment.center,
color: Colors.blue,
width: maxWidth * 0.1,
child: Icon(
isOpen
? Icons.keyboard_arrow_right
: Icons.keyboard_arrow_left,
color: Colors.white,
),
),
),
buildColumn(['INV', 'sin', 'ln', 'π', '(']),
buildColumn(['Deg', 'cos', 'log', 'e', ')']),
],
),
),
),
],
),
);
},
);
}
Column buildColumn(List<String> listBtns) {
return Column(
children: listBtns
.map((btnText) => Expanded(
child: RaisedButton(
color: Colors.blue,
onPressed: () => widget.onAdd(btnText),
child: Text(
btnText,
style: TextStyle(color: Colors.white),
),
),
))
.toList(),
);
}
}
class _FirstLayerKeybord extends StatelessWidget {
const _FirstLayerKeybord({
Key key,
@required this.onAdd,
}) : super(key: key);
final Function(String value) onAdd;
final primaryButtons = const [
['1', '2', '3'],
['4', '5', '6'],
['7', '8', '9'],
['0', '.', '='],
];
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: primaryButtons
.map((row) => Expanded(
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: row
.map((e) => Expanded(
child: RaisedButton(
onPressed: () => onAdd(e),
child: Text(e),
),
))
.toList(),
),
))
.toList());
}
}
class NoKeyboardEditableText extends EditableText {
NoKeyboardEditableText({
@required TextEditingController controller,
@required TextStyle style,
@required Color cursorColor,
bool autofocus = false,
Color selectionColor,
@required NoKeyboardEditableTextFocusNode noKeyboardEditableTextFocusNode,
}) : super(
controller: controller,
focusNode: noKeyboardEditableTextFocusNode,
style: style,
cursorColor: cursorColor,
autofocus: autofocus,
selectionColor: selectionColor,
backgroundCursorColor: Colors.black,
);
@override
EditableTextState createState() {
return NoKeyboardEditableTextState();
}
}
class NoKeyboardEditableTextState extends EditableTextState {
@override
void requestKeyboard() {
FocusScope.of(context).requestFocus(widget.focusNode);
}
}
class NoKeyboardEditableTextFocusNode extends FocusNode {
@override
bool consumeKeyboardToken() {
return false;
}
}
Upvotes: 2
Reputation: 7119
Use Stack
to position the drawer on top of calculator screen.
Use Positioned
for the drawer and set its left
parameter according to the amount that it's pulled.
Set the left
parameter of the drawer to the end of the screen initially.
Use GestureDetector
and onPanUpdate
to change the position when it's pulled.
Change the drawer icon according to the position of the drawer.
For the dim effect on the calculator screen, use a ModalBarrier
. Wrap it with an Opacity
widget and set its opacity
parameter according to the amount the drawer is pulled.
static double _offset = 30;
double _drawerLeft = 400;
IconData _drawerIcon = Icons.arrow_back_ios;
bool _init = true;
@override
Widget build(BuildContext context) {
if (_init) {
_drawerLeft = MediaQuery.of(context).size.width - _offset;
_init = false;
}
return Scaffold(
body: Align(
alignment: Alignment.bottomCenter,
child: FractionallySizedBox(
widthFactor: 1,
heightFactor: 0.5,
child: Stack(
fit: StackFit.expand,
children: <Widget>[
Positioned.fill(
child: Container(
color: Colors.grey[200],
child: Center(
child: Text(
'text',
style: TextStyle(fontSize: 32),
)),
),
),
Positioned.fill(
right: 0,
child: Opacity(
opacity: 1 -
_drawerLeft /
(MediaQuery.of(context).size.width - _offset),
child:
ModalBarrier(dismissible: false, color: Colors.black87),
),
),
Positioned(
width: MediaQuery.of(context).size.width * 3 / 4,
top: 0,
height: MediaQuery.of(context).size.height / 2,
left: _drawerLeft,
child: GestureDetector(
onPanUpdate: (details) {
_drawerLeft += details.delta.dx;
if (_drawerLeft <= MediaQuery.of(context).size.width / 4)
_drawerLeft = MediaQuery.of(context).size.width / 4;
if (_drawerLeft >=
MediaQuery.of(context).size.width - _offset)
_drawerLeft =
MediaQuery.of(context).size.width - _offset;
if (_drawerLeft <= MediaQuery.of(context).size.width / 3)
_drawerIcon = Icons.arrow_forward_ios;
if (_drawerLeft >=
MediaQuery.of(context).size.width - 2 * _offset)
_drawerIcon = Icons.arrow_back_ios;
setState(() {});
},
child: Container(
decoration: BoxDecoration(color: Colors.blue),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Padding(
padding: EdgeInsets.only(right: _offset),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
Icon(
_drawerIcon,
color: Colors.white,
),
],
),
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'text',
style: TextStyle(
color: Colors.white, fontSize: 32),
)
],
)
],
),
)),
),
],
),
),
),
);
}
Result:
Upvotes: 6