pratik97179
pratik97179

Reputation: 183

Volume widget like iOS - Flutter

I want to create a volume control widget like iOS. Below is the picture for reference
enter image description here

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,
        ),
      ],
    ),
  ),
),

And the result is this
enter image description here

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

Answers (2)

lepsch
lepsch

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)

Screenshot

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

Kaushik Chandru
Kaushik Chandru

Reputation: 17792

To ignore the user clicks you can wrap the widget with IgnorePointer.

Upvotes: 1

Related Questions