Reputation: 183
I want to create a volume control widget like iOS. Below is the picture for reference
Is there any way to create the same without using any plugins or packages?
I tried to replicate it but couldn't think of the logic for it. Below is the code that I currently have.
GestureDetector(
child: Container(
height: 150.0,
width: 30.0,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15),
boxShadow: <BoxShadow>[
const BoxShadow(
color: Color(0xffbebebe),
),
BoxShadow(
color: Theme.of(context).primaryColor,
offset: const Offset(1.5, 1.5),
blurRadius: 2.0,
spreadRadius: -2,
),
],
),
),
),
How can I fill the Container
when the user taps on it? I thought of using a Stack
and then looking for the user's local tap position on the Container
, followed by setting the height of volume level indicator Container
up to the local tap position. But the problem is, how will I manage to tap on the areas of Container
that already have been filled, as it would have been overlapped by the current volume level indicator Container
?
Upvotes: 2
Views: 999
Reputation: 10369
It's possible to achieve this with the standard Slider with a custom track shape. To make a vertical Slider
just use the RotatedBox.
Check it out (Also the live demo on DartPad)
Here's the code. The custom track shape was extracted from the already existing RoundedRectSliderTrackShape
and customized to paint the inner shadow in the inactive track.
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: const MyHomePage(),
theme: ThemeData(
scaffoldBackgroundColor: const Color(0xffebecf0),
),
debugShowCheckedModeBanner: false,
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key}) : super(key: key);
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(64.0),
child: Row(
children: [
const RotatedBox(
quarterTurns: -1,
child: SizedBox(
width: 350,
child: InnerShadowSlider(trackHeight: 30),
),
),
const SizedBox(width: 60),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: const [
SizedBox(
width: 300,
child: InnerShadowSlider(),
),
SizedBox(height: 60),
SizedBox(
width: 350,
child: InnerShadowSlider(),
),
],
),
],
),
),
);
}
}
class InnerShadowSlider extends StatefulWidget {
final double trackHeight;
const InnerShadowSlider({Key? key, this.trackHeight = 60}) : super(key: key);
@override
State<InnerShadowSlider> createState() => _InnerShadowSliderState();
}
class _InnerShadowSliderState extends State<InnerShadowSlider> {
var _volume = 0.0;
@override
Widget build(BuildContext context) {
return SliderTheme(
data: SliderTheme.of(context).copyWith(
trackHeight: widget.trackHeight,
overlayShape: SliderComponentShape.noOverlay,
thumbShape: SliderComponentShape.noThumb,
trackShape: const MyRoundedRectSliderTrackShape(),
),
child: Slider(
min: 0,
max: 100,
value: _volume,
onChanged: (value) => setState(() => _volume = value),
inactiveColor: Colors.transparent,
),
);
}
}
class MyRoundedRectSliderTrackShape extends SliderTrackShape
with BaseSliderTrackShape {
const MyRoundedRectSliderTrackShape();
@override
void paint(
PaintingContext context,
Offset offset, {
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required Animation<double> enableAnimation,
required TextDirection textDirection,
required Offset thumbCenter,
bool isDiscrete = false,
bool isEnabled = false,
double additionalActiveTrackHeight = 0,
}) {
assert(sliderTheme.disabledActiveTrackColor != null);
assert(sliderTheme.disabledInactiveTrackColor != null);
assert(sliderTheme.activeTrackColor != null);
assert(sliderTheme.inactiveTrackColor != null);
assert(sliderTheme.thumbShape != null);
// If the slider [SliderThemeData.trackHeight] is less than or equal to 0,
// then it makes no difference whether the track is painted or not,
// therefore the painting can be a no-op.
if (sliderTheme.trackHeight == null || sliderTheme.trackHeight! <= 0) {
return;
}
final trackHeight = sliderTheme.trackHeight!;
// Assign the track segment paints, which are leading: active and
// trailing: inactive.
final ColorTween activeTrackColorTween = ColorTween(
begin: sliderTheme.disabledActiveTrackColor,
end: sliderTheme.activeTrackColor);
final ColorTween inactiveTrackColorTween = ColorTween(
begin: sliderTheme.disabledInactiveTrackColor,
end: sliderTheme.inactiveTrackColor);
final Paint activePaint = Paint()
..color = activeTrackColorTween.evaluate(enableAnimation)!;
final Paint inactivePaint = Paint()
..color = inactiveTrackColorTween.evaluate(enableAnimation)!;
final Paint leftTrackPaint;
final Paint rightTrackPaint;
switch (textDirection) {
case TextDirection.ltr:
leftTrackPaint = activePaint;
rightTrackPaint = inactivePaint;
break;
case TextDirection.rtl:
leftTrackPaint = inactivePaint;
rightTrackPaint = activePaint;
break;
}
final Rect trackRect = getPreferredRect(
parentBox: parentBox,
offset: offset,
sliderTheme: sliderTheme,
isEnabled: isEnabled,
isDiscrete: isDiscrete,
);
activePaint.shader = ui.Gradient.linear(
ui.Offset(trackRect.left, 0),
ui.Offset(thumbCenter.dx, 0),
[
const Color(0xff0f3dea),
const Color(0xff2069f4),
],
);
final Radius trackRadius = Radius.circular(trackRect.height / 2);
final Paint shadow = Paint()..color = const Color(0xffb3b6c7);
context.canvas.clipRRect(
RRect.fromLTRBR(trackRect.left, trackRect.top, trackRect.right,
trackRect.bottom, trackRadius),
);
// Solid shadow color - Top elevation
context.canvas.drawRRect(
RRect.fromLTRBR(trackRect.left, trackRect.top, trackRect.right,
trackRect.bottom, trackRadius),
shadow);
// Bottom elevation
shadow
..color = Colors.white
..maskFilter = MaskFilter.blur(
BlurStyle.normal,
ui.Shadow.convertRadiusToSigma(10),
);
context.canvas.drawRRect(
RRect.fromLTRBR(
trackRect.left - trackHeight,
trackRect.top + trackHeight / 2,
trackRect.right - 0,
trackRect.bottom + trackHeight / 2,
trackRadius,
),
shadow);
// Shadow
shadow
..color = const Color(0xfff0f1f5)
..maskFilter = MaskFilter.blur(
BlurStyle.normal,
ui.Shadow.convertRadiusToSigma(15),
);
context.canvas.drawRRect(
RRect.fromLTRBR(
trackRect.left - trackHeight,
trackRect.top + trackHeight / 8,
trackRect.right - trackHeight / 8,
trackRect.bottom,
trackRadius,
),
shadow);
// Active/Inactive tracks
context.canvas.drawRRect(
RRect.fromLTRBR(
trackRect.left,
(textDirection == TextDirection.ltr)
? trackRect.top - (additionalActiveTrackHeight / 2)
: trackRect.top,
thumbCenter.dx,
(textDirection == TextDirection.ltr)
? trackRect.bottom + (additionalActiveTrackHeight / 2)
: trackRect.bottom,
trackRadius,
),
leftTrackPaint,
);
context.canvas.drawRRect(
RRect.fromLTRBR(
thumbCenter.dx,
(textDirection == TextDirection.rtl)
? trackRect.top - (additionalActiveTrackHeight / 2)
: trackRect.top,
trackRect.right,
(textDirection == TextDirection.rtl)
? trackRect.bottom + (additionalActiveTrackHeight / 2)
: trackRect.bottom,
trackRadius,
),
rightTrackPaint,
);
}
}
Upvotes: 1
Reputation: 17792
To ignore the user clicks you can wrap the widget with IgnorePointer
.
Upvotes: 1