简体   繁体   English

音量小部件,例如 iOS - Flutter

[英]Volume widget like iOS - Flutter

I want to create a volume control widget like iOS.我想创建一个音量控制小部件,如 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,
        ),
      ],
    ),
  ),
),

And the result is this结果是这样的
在此处输入图像描述

How can I fill the Container when the user taps on it?当用户点击它时如何填充Container 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.我想到了使用Stack ,然后在Container上寻找用户的本地抽头 position ,然后将音量指示器Container的高度设置为本地抽头 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 ?但问题是,我将如何设法点击Container已经被填充的区域,因为它会被当前的音量水平指示器Container重叠?

It's possible to achieve this with the standard Slider with a custom track shape.可以使用具有自定义轨道形状的标准Slider来实现这一点。 To make a vertical Slider just use the RotatedBox .要制作垂直Slider只需使用RotatedBox

Check it out (Also the live demo on DartPad )看看(还有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.自定义轨道形状是从现有的RoundedRectSliderTrackShape中提取的,并自定义为在非活动轨道中绘制内部阴影。

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,
    );
  }
}

To ignore the user clicks you can wrap the widget with IgnorePointer .要忽略用户点击,您可以使用IgnorePointer包装小部件。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM