I am currently making a music player, and I made PiP mode widget.
the position of the widget appears to be in the right place,
but when I actually tab buttons they don't work properly.
the clickable area is like down half of the widget.
just like the image above, those 3 buttons only work when I tab the area in the red square.
and the code looks like this:
class AudioTitleOverlayWidget extends StatefulWidget {
final Function onClear;
final Widget widget;
const AudioTitleOverlayWidget(
{Key? key, required this.onClear, required this.widget})
: super(key: key);
#override
_AudioTitleOverlayWidgetState createState() =>
_AudioTitleOverlayWidgetState();
}
class _AudioTitleOverlayWidgetState extends State<AudioTitleOverlayWidget> {
late double width;
late double oldWidth;
late double oldHeight;
late double height;
bool isInPipMode = false;
Offset offset = const Offset(0, 500);
_onExitPipMode() {
Future.microtask(() {
setState(() {
isInPipMode = false;
width = oldWidth;
height = oldHeight;
offset = const Offset(0, 0);
});
});
Future.delayed(const Duration(milliseconds: 250), () {
Get.find<OverlayHandler>().disablePip();
});
}
_onPipMode() {
Future.delayed(const Duration(milliseconds: 100), () {
setState(() {
isInPipMode = true;
width = oldWidth;
height = Constants.VIDEO_TITLE_HEIGHT_PIP - 6.0;
// height = 20;
// offset = Offset(0, oldHeight - height - Constants.BOTTOM_PADDING_PIP);
offset = Offset(0, height * 7.55);
});
});
}
#override
Widget build(BuildContext context) {
final bottomPadding = MediaQuery.of(context).padding.bottom;
oldWidth = width = Get.width;
oldHeight = height = Get.height;
final overlayController = Get.put(OverlayHandler());
final hasBottomTab = overlayController.hasBottomTab;
return GetBuilder<OverlayHandler>(builder: (context) {
if (Get.find<OverlayHandler>().inPipMode != isInPipMode) {
isInPipMode = Get.find<OverlayHandler>().inPipMode;
if (isInPipMode) {
_onPipMode();
} else {
_onExitPipMode();
}
}
if(Get.currentRoute == '/home'){
overlayController.hasBottomTab.value = true;
} else{
overlayController.hasBottomTab.value = false;
}
return Obx(()=>Positioned(
// duration: const Duration(milliseconds: 200),
// curve: Curves.fastOutSlowIn,
left:
Get.isBottomSheetOpen == true
? 10000 //final bottomPadding = MediaQuery.of(context).padding.bottom;
: offset.dx,
top: (isInPipMode == true && hasBottomTab.value == false)
? Get.height - 80 - bottomPadding
: (isInPipMode == true && hasBottomTab.value == true)
? Get.height - 160 - bottomPadding
: (isInPipMode == false && hasBottomTab.value == true)
? bottomPadding
: bottomPadding,
bottom: (isInPipMode == true && hasBottomTab.value == false)
? bottomPadding
: (isInPipMode == true && hasBottomTab.value == true)
? 80 + bottomPadding
: (isInPipMode == false && hasBottomTab.value == true)
? bottomPadding
: bottomPadding,
child: SizedBox(
height: height,
width: width,
child: widget.widget, // the code of the widget is below:
),
));
});
}
}
and the code of the overlay widget in Pip mode:
#override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
backgroundColor: const Color(0xff1c1c1e),
body: GetBuilder<OverlayHandler>(
builder: (getContext) {
// return audioPlayer.playerState(
// builder: (context, realtimePlayingInfos) {
if (!Get.find<OverlayHandler>().inPipMode != true) {
return Stack(
children: [
Positioned(
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () {Get.find<OverlayHandler>().disablePip();},
child: Column(
children: [
Container(
width: Get.width,
height: 80,
.......
)
]
)
)
)
.....
I have tried in several devices, and on android they don't seem to have this issue,
and iphone with home button also don't seem to have this issue.
iphones without home button(iphone x ~) seem to have this issue.
anyone knows about this? any idea how to handle this?
this part was the issue:
the audioPlayer widget returns Scaffold for both PiP widget and the full screen player widget. I have low understanding level of Flutter widgets but returning Scaffold for PiP player was the reason.
the code should be like this:
#override
Widget build(BuildContext context) {
return
!Get.find<OverlayHandler>().inPipMode
? Scaffold(
.... // full screen player
: Stack(
.... // PiP player, not returning Scaffold
Related
I have a function that calculates the amount of CameraPosition zoom required depending on the distance between 2 Google map markers. The method specified in the CameraPosition zoom parameter (zoomIn) is called which in turn calls another async method that returns the distance between the two points (getDistance). Both methods run properly but zoomIn always returns null despite the print statement showing the right calculated value. Is this a synchronization issue?
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Container(
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: GoogleMap(
onMapCreated: addMarkers,
cameraTargetBounds: CameraTargetBounds(createTargetBounds()),
initialCameraPosition: CameraPosition(
target:
LatLng((currentLocation.latitude + latitudesArr[selectedDestination]) / 2,
(currentLocation.longitude + longitudesArr[selectedDestination]) / 2),
zoom: zoomIn(bounds: createTargetBounds(), padding: 200.0)),
),
)
],
);
}
double zoomIn({LatLngBounds bounds, double padding}) {
double zoom;
getDistance(bounds).then((double distance){
print('Distance: $distance');
if (distance > 200){
zoom = 16.0;
} else {
zoom = 18.0;
}
print ('Zoom: $zoom');
});
return zoom;
}
Future<double> getDistance(LatLngBounds bounds) async {
double distance = await Geolocator().distanceBetween(
bounds.southwest.latitude,
bounds.southwest.longitude,
bounds.northeast.latitude,
bounds.northeast.longitude);
print('Distance: $distance');
return distance;
}
The zoomIn function correctly populates the zoom variable with 16.0 or 18.0 depending on the distance between the two markers. However, null is always returned.
Ok, I figured that the correct way of doing this is to hard-code the zoom value when the Google map widget is declared, for example:
class MapState extends State<MapRoute> {
#override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
GoogleMap(
onMapCreated: addMarkers,
myLocationEnabled: true,
cameraTargetBounds: CameraTargetBounds(createTargetBounds()),
initialCameraPosition: CameraPosition(
target:
LatLng((currentLocation.latitude + latitudesArr[selectedDestination]) / 2,
(currentLocation.longitude + longitudesArr[selectedDestination]) / 2),
zoom: 18.0),
),
Then, in this example, on the addMarkers method, the following code will wait until GeoLocator returns a distance and only then it can be determined the desired level of zoom. After that is only a matter of calling the map controller's moveCamera(CameraUpdate.zoomTo(zoom)) as follows:
await Geolocator().distanceBetween(
bounds.southwest.latitude,
bounds.southwest.longitude,
bounds.northeast.latitude,
bounds.northeast.longitude)
.then((distance) {
print('The Distance: $distance');
double zoom;
if (distance > 300) {
zoom = 17.0;
} else if (distance > 100){
zoom = 18.0;
} else {
zoom = 19.0;
}
print ('Zoom: $zoom');
controller.moveCamera(CameraUpdate.zoomTo(zoom));
});
I can't get the set state to change in multiple areas from an onTap VoidCallback?
I have two AnimationController in different stateful widgets. What I would like to implement is that if controller1.value == 0.0 then it'll make sure that when onTap that the controller2.value == 1.0 and visa versa for if controller1.value == 1.0.
StatefulWidget bottom layer (passes the widget.onTapOpen/closed to the top layer)
onTap: () {
_toggleExpandingSheetPanelVisibility();
setState(() {
if (_controller1.value == 0.0){
widget.onTapOpen();
}
else if (_controller1.value == 1.0){
widget.onTapClosed();
}
});
},
StatefulWidget top layer
onTapOpen: _ensureVisible,
onTapClosed: _ensureInvisible,
void _ensureVisible() {
setState(() {
if (_controller2.value == 0.0) {
_toggleVisibility();
}
});
}
void _ensureInvisible() {
setState(() {
if (_controller2.value == 1.0) {
_toggleVisibility();
}
});
}
It worked when if (_controller2.value > or < 0.5) then it would toggle the visibility.
I want to listen to firebase upload success event, so that I can give a confirmation to the user about the completion of the particular event.
How to listen success event?
In debug console I'm getting the below information.
I/flutter (22734): EVENT StorageTaskEventType.progress
I/flutter (22734): EVENT StorageTaskEventType.progress
I/flutter (22734): EVENT StorageTaskEventType.progress
I/flutter (22734): EVENT StorageTaskEventType.success
My upload code looks like this.
final StorageReference firebaseStoragereference =
FirebaseStorage
.instance
.ref()
.child('images/myImage1.jpg' );
final StorageUploadTask task = firebaseStoragereference.putFile(sampleImage);
You can do it this way:
final StorageUploadTask uploadTask = ref.putFile(file);
StorageTaskSnapshot taskSnapshot = await uploadTask.onComplete;
String downloadUrl = await taskSnapshot.ref.getDownloadURL();
In Order to Listen to event, you Need to listen to the Events Streams & you Can Use StreamBuilder to Show the Status.
An Minimal E.g: You Modify to As per your Own Need like- show Progress Bar. In your Build Method where you want show the Status call the Function - _uploadStatus(task);
String _bytesTransferred(StorageTaskSnapshot snapshot) {
double res = snapshot.bytesTransferred / 1024.0;
double res2 = snapshot.totalByteCount / 1024.0;
return '${res.truncate().toString()}/${res2.truncate().toString()}';
}
Widget _uploadStatus(StorageUploadTask task) {
return StreamBuilder(
stream: task.events,
builder: (BuildContext context, snapshot) {
Widget subtitle;
if (snapshot.hasData) {
final StorageTaskEvent event = snapshot.data;
final StorageTaskSnapshot snap = event.snapshot;
subtitle = Text('${_bytesTransferred(snap)} KB sent');
} else {
subtitle = const Text('Starting...');
}
return ListTile(
title: s.isComplete && s.isSuccessful
? Text(
'Done',
style: detailStyle,
)
: Text(
'Uploading',
style: detailStyle,
),
subtitle: subtitle,
);
},
);
}
#anmol.majhail I improved your calc-function like this:
String _bytesTransferred(StorageTaskSnapshot snapshot) {
double res = (snapshot.bytesTransferred / 1024.0) / 1000;
double res2 = (snapshot.totalByteCount / 1024.0) / 1000;
return '${res.toStringAsFixed(2)}/${res2.toStringAsFixed(2)}';
}
With this it will look like this:
3,75/30,32 MB sent
Here for Flutter-Web:
String _bytesTransferred(fb.UploadTaskSnapshot snapshot) {
double res = (snapshot.bytesTransferred / 1024.0) / 1000;
double res2 = (snapshot.totalBytes / 1024.0) / 1000;
return '${res.toStringAsFixed(2)}/${res2.toStringAsFixed(2)}';
}
Widget _uploadStatus(fb.UploadTask task, String startingText, String doneText,
String uploadingText) {
return StreamBuilder<fb.UploadTaskSnapshot>(
stream: task.onStateChanged,
builder: (BuildContext context, snapshot) {
Widget subtitle;
if (snapshot.hasData) {
final fb.UploadTaskSnapshot snap = snapshot.data;
subtitle = Text('${_bytesTransferred(snap)} MB');
} else {
subtitle = Text(startingText);
}
return ListTile(
title: snapshot.data.state == fb.TaskState.SUCCESS
? Text(doneText)
: Text(uploadingText),
subtitle: subtitle,
);
},
);
}
I am developing an application at the moment where each StateFull widget is listening on a Stream within it's initState function. The data received from the Stream is then updated in the widgets state.
The problem is, after 1min or less, the graphical interface no longer updates. However, the stream is still receiving data, which I can tell by putting a print function in the listen function.
Please note, the stream is from StreamController().stream.asBroadcastStream() as I need to listen to the same streams from multiple widgets. If it helps, Here is the API to the data I am receiving, along with their timings. Each packet is in it's own stream.
Edit: For example,
class DeltaInfo extends StatefulWidget{
DeltaInfoState createState()=> DeltaInfoState();
}
class DeltaInfoState extends State<DeltaInfo> {
LapData playerCarLapData;
LapData carBehindLapData;
LapData carInFrontLapData;
int carInFrontIndex;
int carBehindIndex;
int numberOfRacers = 20;
int session = SessionStatus.raceOne;
#override initState(){
super.initState();
sessionStream.asBroadcastStream().listen((PacketSessionInfo packet){
session = packet.sessionType;
sessionStream.drain();
});
lapDataStream.asBroadcastStream().listen((PacketLapData packet){
if(SessionStatus.isRace(sessionType: session)){
numberOfRacers = 0;
playerCarLapData = packet.lapData[packet.headder.playerCarIndex];
int i = 0;
carInFrontLapData = null;
carBehindLapData = null;
carInFrontIndex = null;
carBehindIndex = null;
packet.lapData.forEach((LapData car){
if(car == null) return;
if(ResultStatus.isInRace(resultStatus: car.resultStatus)){
this.numberOfRacers++;
}
if(car.carPosistion == playerCarLapData.carPosistion - 1){
carInFrontIndex = i;
carInFrontLapData = car;
}
if(car.carPosistion == playerCarLapData.carPosistion + 1) {
carBehindIndex = i;
carBehindLapData = car;
}
i++;
});
carTelemtryStream.asBroadcastStream().listen((PacketCarTelemtryData packet){
CarTelemtryData playersCar = packet.carTelemtryData[packet.headder.playerCarIndex];
if(carInFrontIndex != null){
CarTelemtryData carInFront = packet.carTelemtryData[carInFrontIndex];
double time = calculateDelta(
carALapData: carInFrontLapData,
carBLapData: playerCarLapData,
carATelemtry: carInFront,
carBTelemtry: playersCar,
carAInfront: true
);
setState((){
inFrontValue = "+ ${time.toStringAsFixed(3)}";
});
} else {
setState((){
inFrontValue = "In lead";
});
}
if(carBehindIndex != null){
CarTelemtryData carBehind = packet.carTelemtryData[carBehindIndex];
setState((){
double time = calculateDelta(
carALapData: carBehindLapData,
carBLapData: playerCarLapData,
carATelemtry: carBehind,
carBTelemtry: playersCar,
carAInfront: false
);
behindValue = "- ${time.toStringAsFixed(3)}";
});
} else {
setState((){
behindValue = "Last Place";
});
}
});
} else {
LapData player = packet.lapData[packet.headder.playerCarIndex];
Map<String, int> currentTime = timeInfoFromSeconds(player.currentLapTime);
int mins = currentTime["mins"];
String seconds = currentTime["seconds"].toString().padLeft(2, '0');
// String ms = (player.currentLapTime % 1).toString().substring(2, 5);
String ms = "000";
setState((){
inFrontValue = "$mins:$seconds.$ms";
});
Map<String, int> bestTime = timeInfoFromSeconds(player.bestLapTime);
mins = bestTime["mins"];
seconds = bestTime["seconds"].toString().padLeft(2, '0');
ms = (player.bestLapTime % 1).toString().substring(2, 5);
setState((){
behindValue = "$mins:$seconds.$ms";
});
}
lapDataStream.drain();
});
}
String inFrontValue = "A";
String behindValue = "A";
#override Widget build(BuildContext context){
return Expanded(
child: Column(
children: [
Expanded(
child: FittedBox(
child: Text("$inFrontValue"),
),
),
Expanded(
child: FittedBox(
child: Text("$behindValue"),
),
),
]
)
);
}
}
double calculateDelta(
{#required LapData carALapData,
#required LapData carBLapData,
#required CarTelemtryData carATelemtry,
#required CarTelemtryData carBTelemtry,
#required bool carAInfront}
){
if(carAInfront){
double distanceDifference = carALapData.totalDistance - carBLapData.totalDistance;
double carBSpeed = kphToMs(carBTelemtry.speed);
return distanceDifference/carBSpeed;
} else {
double distanceDifference = carBLapData.totalDistance - carALapData.totalDistance;
double carASpeed = kphToMs(carATelemtry.speed);
return distanceDifference/carASpeed;
}
}
double kphToMs(int kph){
return kph*0.2777778;
}
And then the stream is define as the following
StreamController<PacketLapData> _lapDataStream = StreamController<PacketLapData>();
Stream<PacketLapData> lapDataStream = _lapDataStream.stream.asBroadcastStream();
In the end, Instead of listening to the stream on initState, I used a StreamBuilder in the build function instead. This seams to have stopped the lag/freezing.
I'm trying to add a date mask to a textField since I did not like the Date Picker because for date of birth, for example, it is not as nimble.
After that, converting from string to datetime, I believe I can continue the project,
Thanks in advance.
static final TextEditingController _birthDate = new TextEditingController();
new TextFormField(
controller: _birthDate,
maxLength: 10,
keyboardType: TextInputType.datetime,
validator: _validateDate
), String _validateDate(String value) {
if(value.isEmpty)
return null;
if(value.length != 10)
return 'Enter date in DD / MM / YYYY format';
return null;
}
I modified some things and managed to get the expected result.
I created this class to define the variable
static final _UsNumberTextInputFormatter _birthDate = new _UsNumberTextInputFormatter();
class _UsNumberTextInputFormatter extends TextInputFormatter {
#override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue ) {
final int newTextLength = newValue.text.length;
int selectionIndex = newValue.selection.end;
int usedSubstringIndex = 0;
final StringBuffer newText = new StringBuffer();
if (newTextLength >= 3) {
newText.write(newValue.text.substring(0, usedSubstringIndex = 2) + '/');
if (newValue.selection.end >= 2)
selectionIndex ++;
}
if (newTextLength >= 5) {
newText.write(newValue.text.substring(2, usedSubstringIndex = 4) + '/');
if (newValue.selection.end >= 4)
selectionIndex++;
}
if (newTextLength >= 9) {
newText.write(newValue.text.substring(4, usedSubstringIndex = 8));
if (newValue.selection.end >= 8)
selectionIndex++;
}
// Dump the rest.
if (newTextLength >= usedSubstringIndex)
newText.write(newValue.text.substring(usedSubstringIndex));
return new TextEditingValue(
text: newText.toString(),
selection: new TextSelection.collapsed(offset: selectionIndex),
);
}
}
And finally I added an inputformat to the textfield
new TextFormField(
maxLength: 10,
keyboardType: TextInputType.datetime,
validator: _validateDate,
decoration: const InputDecoration(
hintText: 'Digite sua data de nascimento',
labelText: 'Data de Nascimento',
),
inputFormatters: <TextInputFormatter> [
WhitelistingTextInputFormatter.digitsOnly,
// Fit the validating format.
_birthDate,
]
),
Now it's all right, thank you
https://pub.dartlang.org/packages/masked_text
masked_text
A package for masked texts, so if you want a mask for phone, or zip code or any kind of mask, just use it :D
Getting Started
It's very simple, it's a Widget as all the other ones.
new MaskedTextField
(
maskedTextFieldController: _textCPFController,
mask: "xx/xx/xxxx",
maxLength: 10,
keyboardType: TextInputType.number,
inputDecoration: new InputDecoration(
hintText: "Type your birthdate", labelText: "Date"),
);
'x' is the normal char that your text will have.
this sample reproduces something like this in the end: 11/02/1995.
My solution:
class MaskTextInputFormatter extends TextInputFormatter {
final int maskLength;
final Map<String, List<int>> separatorBoundries;
MaskTextInputFormatter({
String mask = "xx.xx.xx-xxx.xx",
List<String> separators = const [".", "-"],
}) : this.separatorBoundries = {
for (var v in separators)
v: mask.split("").asMap().entries.where((entry) => entry.value == v).map((e) => e.key).toList()
},
this.maskLength = mask.length;
#override
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
final int newTextLength = newValue.text.length;
final int oldTextLength = oldValue.text.length;
// removed char
if (newTextLength < oldTextLength) return newValue;
// maximum amount of chars
if (oldTextLength == maskLength) return oldValue;
// masking
final StringBuffer newText = StringBuffer();
int selectionIndex = newValue.selection.end;
// extra boundaries check
final separatorEntry1 = separatorBoundries.entries.firstWhereOrNull((entry) => entry.value.contains(oldTextLength));
if (separatorEntry1 != null) {
newText.write(oldValue.text + separatorEntry1.key);
selectionIndex++;
} else {
newText.write(oldValue.text);
}
// write the char
newText.write(newValue.text[newValue.text.length - 1]);
return TextEditingValue(
text: newText.toString(),
selection: TextSelection.collapsed(offset: selectionIndex),
);
}
}
This solution checks when date is out of range (eg. there is not month like 13). It's super inefficient, but it works.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class DateFormatter extends TextInputFormatter {
final String mask = 'xx-xx-xxxx';
final String separator = '-';
#override
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
if(newValue.text.length > 0) {
if(newValue.text.length > oldValue.text.length) {
String lastEnteredChar = newValue.text.substring(newValue.text.length-1);
if(!_isNumeric(lastEnteredChar)) return oldValue;
if(newValue.text.length > mask.length) return oldValue;
if(newValue.text.length < mask.length && mask[newValue.text.length - 1] == separator) {
String value = _validateValue(oldValue.text);
print(value);
return TextEditingValue(
text: '$value$separator$lastEnteredChar',
selection: TextSelection.collapsed(
offset: newValue.selection.end + 1,
),
);
}
if(newValue.text.length == mask.length) {
return TextEditingValue(
text: '${_validateValue(newValue.text)}',
selection: TextSelection.collapsed(
offset: newValue.selection.end,
),
);
}
}
}
return newValue;
}
bool _isNumeric(String s) {
if(s == null) return false;
return double.parse(s, (e) => null) != null;
}
String _validateValue(String s) {
String result = s;
if (s.length < 4) { // days
int num = int.parse(s.substring(s.length-2));
String raw = s.substring(0, s.length-2);
if (num == 0) {
result = raw + '01';
} else if (num > 31) {
result = raw + '31';
} else {
result = s;
}
} else if (s.length < 7) { // month
int num = int.parse(s.substring(s.length-2));
String raw = s.substring(0, s.length-2);
if (num == 0) {
result = raw + '01';
} else if (num > 12) {
result = raw + '12';
} else {
result = s;
}
} else { // year
int num = int.parse(s.substring(s.length-4));
String raw = s.substring(0, s.length-4);
if (num < 1950) {
result = raw + '1950';
} else if (num > 2006) {
result = raw + '2006';
} else {
result = s;
}
}
print(result);
return result;
}
}
You can now use the boolean property, "obscureText" of TextField to mask input.