Flutter custom range slider

Viewed 6533

I'm trying to create a range slider on top of a Row of Containers which should create an audio waveform, but I have no idea where to even start...

The main issue is that the range slider sits right on top of the row of containers and it should change their colors on the "selected" section.

enter image description here

Here's what I currently have:

enter image description here

The code to create the image and details.

class BeatLyricsPage extends StatefulWidget {
  final Beat beat;
  BeatLyricsPage(this.beat);

  @override
  _BeatLyricsPageState createState() => _BeatLyricsPageState(beat);
}

class _BeatLyricsPageState extends State<BeatLyricsPage> {
  final Beat beat;


  final _kPicHeight = 190.0;
  // used in _buildPageHeading to add the beat key and beat bpm
  Widget _buildBeatInfoItem(String text) => DecoratedBox(
        decoration: BoxDecoration(
          border: Border.all(color: MyColor.white, width: 1.0),
          borderRadius: BorderRadius.circular(4.0),
        ),
        child: Padding(
          padding: EdgeInsets.symmetric(vertical: 3.0, horizontal: 12.0),
          child: Text(text, style: TextStyle(color: MyColor.white, fontSize: 10.0, fontWeight: FontWeight.w600)),
        ),
      );

  final _kAudioControlsWidth = 180.0;
  final _kAudioControlsHeight = 36.0;
  final _kAudioControlsMainButtonSize = 56.0;

  Widget _buildAudioControls(BuildContext context) => Positioned(
        left: (MediaQuery.of(context).size.width / 2) - (_kAudioControlsWidth / 2),
        top: _kPicHeight - (_kAudioControlsHeight / 2),
        child: Stack(
          overflow: Overflow.visible,
          children: [
            Container(
              width: _kAudioControlsWidth,
              height: _kAudioControlsHeight,
              decoration: BoxDecoration(color: MyColor.darkGrey, borderRadius: BorderRadius.circular(100.0)),
              padding: EdgeInsets.symmetric(horizontal: LayoutSpacing.sm),
              child: Row(
                children: [
                  CButtonLike(beatId: beat.id),
                  Spacer(),
                  GestureDetector(
                    behavior: HitTestBehavior.opaque,
                    child: Icon(BeatPulseIcons.cart),
                    onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => LicenseOptionsPage(beat))),
                  ),
                ],
              ),
            ),
            // ****** MAIN BUTTON (Play/Pause) ******
            Positioned(
              left: (_kAudioControlsWidth / 2) - (_kAudioControlsMainButtonSize / 2),
              top: (_kAudioControlsHeight - _kAudioControlsMainButtonSize) / 2,
              child: Container(
                height: _kAudioControlsMainButtonSize,
                width: _kAudioControlsMainButtonSize,
                decoration: BoxDecoration(
                    gradient: LinearGradient(begin: Alignment.topLeft, colors: [MyColor.primary, Color(0xFFf80d0a)]),
                    borderRadius: BorderRadius.circular(100.0)),
                child: CButtonPlay(),
              ),
            )
          ],
        ),
      );

  Widget _buildWaveForm() {
    // creates a random list of doubles, "fake data"
    var rng = Random();
    final List waveFormData = [];
    for (var i = 0; i < 90; i++) {
      waveFormData.add(rng.nextInt(45).toDouble());
    }
    // player bloc
    final playerBloc = BlocProvider.getPlayerBloc(context);
    // renders
    return Container(
      height: _kPicHeight,
      padding: EdgeInsets.symmetric(vertical: LayoutSpacing.xxxl),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        crossAxisAlignment: CrossAxisAlignment.end,
        children: [
          // current playing second
          StreamBuilder<double>(
            stream: playerBloc.playingSecond,
            initialData: 0.0,
            builder: (_, playingSecondSnapshot) {
              // current beat playing
              return StreamBuilder<Beat>(
                stream: playerBloc.playingBeat,
                builder: (_, playingBeatSnapshot) {
                  final playingBeat = playingBeatSnapshot.data;
                  // if the beat playing is the same as the beat selected for the lyrics, show playing seconds
                  if (playingBeat?.id == beat.id)
                    return Text(secondsToTime(playingSecondSnapshot.data), style: MyFontStyle.sizeXxs);
                  // otherwise show 0:00
                  else
                    return Text(secondsToTime(0), style: MyFontStyle.sizeXxs);
                },
              );
            },
          ),
          SizedBox(width: LayoutSpacing.xs),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.end,
            children: waveFormData
                .map((waveFormDataIndex) => Container(
                      height: waveFormDataIndex > 5.0 ? waveFormDataIndex : 5.0,
                      width: 2,
                      color: MyColor.white,
                      margin: EdgeInsets.only(right: 1),
                    ))
                .toList(),
          ),
          SizedBox(width: LayoutSpacing.xs),
          Text(secondsToTime(beat.length), style: MyFontStyle.sizeXxs),
        ],
      ),
    );
  }

  Widget _buildPageHeading(BuildContext context, {@required String imageUrl}) => Stack(
        children: [
          Column(
            children: [
              Hero(
                tag: MyKeys.makePlayerCoverKey(beat.id),
                child: Opacity(
                  opacity: 0.3,
                  child: Container(
                    height: _kPicHeight,
                    decoration: BoxDecoration(
                      image: DecorationImage(image: CachedNetworkImageProvider(imageUrl), fit: BoxFit.cover),
                    ),
                  ),
                ),
              ),
              Container(color: MyColor.background, height: LayoutSpacing.xl)
            ],
          ),
          Padding(
            padding: EdgeInsets.all(LayoutSpacing.xs),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.end,
              children: [
                _buildBeatInfoItem(beat.key),
                SizedBox(width: 4.0),
                _buildBeatInfoItem('${beat.bpm} BPM'),
              ],
            ),
          ),
          _buildAudioControls(context),
          _buildWaveForm(),
        ],
      );
}
2 Answers

To create a custom range slider, you can use the GestureRecognizer and save the position of each slider in variable inside a StatefulWidget. To decide wether a bar with the index i is inside the range, you can divide the pixel position of the limiter(bar1&bar2 in the source below) by the width of bars and compare it to i.

Sadly I couldn't work with your code example. Instead I created a bare minimum example as you can see below. If you take a minute to read into, I'm sure you can transfer it to your application.

Custom Range Slider

import 'dart:math';

import 'package:flutter/material.dart';

List<int> bars = [];

void main() {
  // generate random bars
  Random r = Random();
  for (var i = 0; i < 50; i++) {
    bars.add(r.nextInt(200));
  }

  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Home(),
    );
  }
}

class Home extends StatefulWidget {
  @override
  State<StatefulWidget> createState() => HomeState();
}

class HomeState extends State<Home> {
  static const barWidth = 5.0;
  double bar1Position = 60.0;
  double bar2Position = 180.0;

  @override
  Widget build(BuildContext context) {
    int i = 0;

    return Scaffold(
      body: Center(
        child: Stack(
          alignment: Alignment.centerLeft,
          children: <Widget>[
            Row(
              crossAxisAlignment: CrossAxisAlignment.center,
              mainAxisAlignment: MainAxisAlignment.start,
              children: bars.map((int height) {
                Color color =
                    i >= bar1Position / barWidth && i <= bar2Position / barWidth
                        ? Colors.deepPurple
                        : Colors.blueGrey;
                i++;

                return Container(
                  color: color,
                  height: height.toDouble(),
                  width: 5.0,
                );
              }).toList(),
            ),
            Bar(
              position: bar2Position,
              callback: (DragUpdateDetails details) {
                setState(() {
                  bar2Position += details.delta.dx;
                });
              },
            ),
            Bar(
              position: bar1Position,
              callback: (DragUpdateDetails details) {
                setState(() {
                  bar1Position += details.delta.dx;
                });
              },
            ),
          ],
        ),
      ),
    );
  }
}

class Bar extends StatelessWidget {
  final double position;
  final GestureDragUpdateCallback callback;

  Bar({this.position, this.callback});

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: EdgeInsets.only(left: position >= 0.0 ? position : 0.0),
      child: GestureDetector(
        onHorizontalDragUpdate: callback,
        child: Container(
          color: Colors.red,
          height: 200.0,
          width: 5.0,
        ),
      ),
    );
  }
}

in order to have a wave slider :

class WaveSlider extends StatefulWidget {
  final double initialBarPosition;
  final double barWidth;
  final int maxBarHight;
  final double width;
  WaveSlider({
    this.initialBarPosition = 0.0,
    this.barWidth = 5.0,
    this.maxBarHight = 50,
    this.width = 60.0,
  });
  @override
  State<StatefulWidget> createState() => WaveSliderState();
}

class WaveSliderState extends State<WaveSlider> {
  List<int> bars = [];
  double barPosition;
  double barWidth;
  int maxBarHight;
  double width;

  int numberOfBars;

  void randomNumberGenerator() {
    Random r = Random();
    for (var i = 0; i < numberOfBars; i++) {
      bars.add(r.nextInt(maxBarHight - 10) + 10);
    }
  }

  _onTapDown(TapDownDetails details) {
    var x = details.globalPosition.dx;
    print("tap down " + x.toString());
    setState(() {
      barPosition = x;
    });
  }

  @override
  void initState() {
    super.initState();
    barPosition = widget.initialBarPosition;
    barWidth = widget.barWidth;
    maxBarHight = widget.maxBarHight.toInt();
    width = widget.width;
    if (bars.isNotEmpty) bars = [];
    numberOfBars = width ~/ barWidth;
    randomNumberGenerator();
  }

  @override
  Widget build(BuildContext context) {
    int barItem = 0;
    return Scaffold(
      backgroundColor: Colors.grey[900],
      body: Center(
        child: GestureDetector(
          onTapDown: (TapDownDetails details) => _onTapDown(details),
          onHorizontalDragUpdate: (DragUpdateDetails details) {
            setState(() {
              barPosition = details.globalPosition.dx;
            });
          },
          child: Container(
            child: Row(
              crossAxisAlignment: CrossAxisAlignment.end,
              mainAxisAlignment: MainAxisAlignment.start,
              children: bars.map((int height) {
                Color color = barItem + 1 < barPosition / barWidth
                    ? Colors.white
                    : Colors.grey[600];
                barItem++;
                return Row(
                  children: <Widget>[
                    Container(
                      width: .1,
                      height: height.toDouble(),
                      color: Colors.black,
                    ),
                    Container(
                      decoration: BoxDecoration(
                        color: color,
                        borderRadius: BorderRadius.only(
                          topLeft: const Radius.circular(1.0),
                          topRight: const Radius.circular(1.0),
                        ),
                      ),
                      height: height.toDouble(),
                      width: 4.8,
                    ),
                    Container(
                      width: .1,
                      height: height.toDouble(),
                      color: Colors.black,
                    ),
                  ],
                );
              }).toList(),
            ),
          ),
        ),
      ),
    );
  }
}

and use it like :

WaveSlider(
        initialBarPosition: 180.0,
        barWidth: 5.0,
        maxBarHight: 50,
        width: MediaQuery.of(context).size.width,
      )
Related