{"id":390364,"date":"2024-06-29T09:12:21","date_gmt":"2024-06-29T09:12:21","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=390364"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=390364","title":{"rendered":"<span>How to create a custom Scrollbar in Flutter using RenderShiftedBox<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>In my project, I was faced with the need to implement a scroll bar. The standard approaches didn&#8217;t fully satisfy what designers wanted to see. Here is an example, it is slightly different in appearance from what I needed, but the essence is approximately the same: the scroll bar should be on the right side of the list and take into account the padding of the scroll bar, with the slider displaying the percentage of the scrolled part of the list.<\/p>\n<figure class=\"\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w780q1\/getpro\/habr\/upload_files\/f1e\/c52\/9b3\/f1ec529b395ea040ed27a2266e8ca3ec.jpg\" width=\"320\" height=\"640\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/f1e\/c52\/9b3\/f1ec529b395ea040ed27a2266e8ca3ec.jpg\" data-blurred=\"true\"\/><figcaption><\/figcaption><\/figure>\n<p>There are several approaches to creating Flutter widgets: composition, <em>CustomPainter<\/em>, and <em>RenderObject<\/em> or any of its subclasses. In most cases, you can get by with composition and cover the rest with CustomPainter, but the most flexible and controllable method is using <em>RenderObject<\/em>.<\/p>\n<p>Let&#8217;s see an example of how you can implement a custom scroll bar using the <em>RenderShiftedBox<\/em>. A couple of words why exactly this one: first of all refer to <a href=\"https:\/\/api.flutter.dev\/flutter\/rendering\/RenderBox-class.html\" rel=\"noopener noreferrer nofollow\">Flutter documentation<\/a> the widget wrapped in our custom scrollbar should be smaller than a parent (i.e. it will resize child widget) to allow scrollbar to be placed near and not over child widget, see what works for us:<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/e08\/384\/d27\/e08384d2726f659f3d9ed32691847b4b.png\" width=\"1804\" height=\"330\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/e08\/384\/d27\/e08384d2726f659f3d9ed32691847b4b.png\"\/><figcaption><\/figcaption><\/figure>\n<p>It follows from the description that it is <em>RenderShiftedBox<\/em> that we need to use.<\/p>\n<h2>Implementation<\/h2>\n<p>Now let&#8217;s start implementing a custom scroll bar, it&#8217;s very simple:<\/p>\n<pre><code class=\"dart\">class CustomScrollbar extends SingleChildRenderObjectWidget {   final ScrollController controller;   final Widget child;   final double? strokeWidth;   final EdgeInsets? padding;   final Color? trackColor;   final Color? thumbColor;    const CustomScrollbar({     Key? key,     required this.controller,     required this.child,     this.strokeWidth,     this.padding,     this.trackColor,     this.thumbColor,   }) : super(key: key);    @override   RenderObject createRenderObject(BuildContext context) {     return RenderCustomScrollbar(       controller: controller,       strokeWidth: strokeWidth ?? 16,       padding:           padding ?? const EdgeInsets.symmetric(             vertical: 16,              horizontal: 8,           ),       trackColor: trackColor ?? Colors.purpleAccent.withOpacity(0.3),       thumbColor: thumbColor ?? Colors.purpleAccent,     );   }    @override   void updateRenderObject(       BuildContext context, covariant RenderCustomScrollbar renderObject) {     if (strokeWidth != null) {       renderObject.strokeWidth = strokeWidth!;     }     if (padding != null) {       renderObject.padding = padding!;     }     if (trackColor != null) {       renderObject.trackColor = trackColor!;     }     if (thumbColor != null) {       renderObject.thumbColor = thumbColor!;     }   } }<\/code><\/pre>\n<p>Since our scrollbar has one child widget, that is why <em>CustomScrollbar<\/em> extends <em>SingleChildRenderObjectWidget<\/em> and override the <em>updateRenderObject()<\/em> and <em>createRenderObject()<\/em> methods to return a new instance of <em>RenderCustomScrollbar<\/em>.<\/p>\n<p>Now let&#8217;s move on to describing the <em>RenderShiftedBox<\/em> itself.<\/p>\n<p>Initialize the constructor and define a listener that will trigger the method to update the slider position, don&#8217;t forget to include <em>markNeedsPaint()<\/em> and <em>markNeedsSemanticsUpdate()<\/em> in that method so that the slider can be redrawn.<\/p>\n<p>\u00a0You can rewrite the methods for calculating the position, the height of the slider as you need, don&#8217;t forget to take into account the paddings, in general, you can make the runner move even diagonally. In my case, the height of the runner depends on the height of the content, in your case, the height of the slider can be fixed or even change dynamically, here you are not limited to anything. I&#8217;ve looked at the calculation of the position of the slider in the standard <em>RawScrollbar<\/em> and adapted it a little bit.<\/p>\n<p>Now let&#8217;s go to the very important function <em>performLayout()<\/em>:<\/p>\n<pre><code class=\"dart\">@override   void performLayout() {     size = constraints.biggest;     if (child == null) return;     child!.layout(constraints.copyWith(maxWidth: _getChildMaxWidth()),         parentUsesSize: !constraints.isTight);     final BoxParentData childParentData = child!.parentData! as BoxParentData;     childParentData.offset = Offset.zero;   }    double _getChildMaxWidth() {     return constraints.maxWidth - padding.horizontal - strokeWidth;   }<\/code><\/pre>\n<p>You need to override the <em>performLayout()<\/em> method and set a size for the <em>RenderBox<\/em>, otherwise, an exception will be thrown that the <em>RenderBox<\/em> hasn&#8217;t set its dimensions. You can take the size from constraints and then override the size of the child widget and its position, from where it starts to be drawn. Here you can also change the size or position of the child widget if you want. In my case <em>Offset.zero<\/em> because I only needed to limit the width of the child widget.<\/p>\n<p>After you&#8217;ve got all positions calculated according to paddings and any other optional parameters, you can start drawing, to do that we need to override the <em>paint()<\/em> method.<\/p>\n<pre><code class=\"dart\">@override   void paint(PaintingContext context, Offset offset) {     if (child == null) return;     context.paintChild(child!, offset);     _resetThumbStartPointIfNeeded();     _trackPaint(context, offset);     _thumbPaint(context, offset);     _textPaint(context, offset);   }<\/code><\/pre>\n<p>The <em>context.paintChild()<\/em> will draw the child widget, it already knows its dimensions. Then we draw the track, the slider, and the text in turn. There&#8217;s no need to go into detail about these methods in this article, you can read the code at this <a href=\"https:\/\/github.com\/safchemist\/custom_scrollbar\" rel=\"noopener noreferrer nofollow\">link<\/a>. In general, in the paint method, you can draw whatever you need.\u00a0<\/p>\n<h2>Result<\/h2>\n<div class=\"tm-iframe_temp\" data-src=\"https:\/\/embedd.srv.habr.com\/iframe\/611fac58920526a288c7e1db\" data-style=\"\" id=\"611fac58920526a288c7e1db\" width=\"\"><\/div>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/574296\/\"> https:\/\/habr.com\/ru\/articles\/574296\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>In my project, I was faced with the need to implement a scroll bar. The standard approaches didn&#8217;t fully satisfy what designers wanted to see. Here is an example, it is slightly different in appearance from what I needed, but the essence is approximately the same: the scroll bar should be on the right side of the list and take into account the padding of the scroll bar, with the slider displaying the percentage of the scrolled part of the list.<\/p>\n<figure class=\"\"><figcaption><\/figcaption><\/figure>\n<p>There are several approaches to creating Flutter widgets: composition, <em>CustomPainter<\/em>, and <em>RenderObject<\/em> or any of its subclasses. In most cases, you can get by with composition and cover the rest with CustomPainter, but the most flexible and controllable method is using <em>RenderObject<\/em>.<\/p>\n<p>Let&#8217;s see an example of how you can implement a custom scroll bar using the <em>RenderShiftedBox<\/em>. A couple of words why exactly this one: first of all refer to <a href=\"https:\/\/api.flutter.dev\/flutter\/rendering\/RenderBox-class.html\" rel=\"noopener noreferrer nofollow\">Flutter documentation<\/a> the widget wrapped in our custom scrollbar should be smaller than a parent (i.e. it will resize child widget) to allow scrollbar to be placed near and not over child widget, see what works for us:<\/p>\n<figure class=\"full-width\"><figcaption><\/figcaption><\/figure>\n<p>It follows from the description that it is <em>RenderShiftedBox<\/em> that we need to use.<\/p>\n<h2>Implementation<\/h2>\n<p>Now let&#8217;s start implementing a custom scroll bar, it&#8217;s very simple:<\/p>\n<pre><code class=\"dart\">class CustomScrollbar extends SingleChildRenderObjectWidget {   final ScrollController controller;   final Widget child;   final double? strokeWidth;   final EdgeInsets? padding;   final Color? trackColor;   final Color? thumbColor;    const CustomScrollbar({     Key? key,     required this.controller,     required this.child,     this.strokeWidth,     this.padding,     this.trackColor,     this.thumbColor,   }) : super(key: key);    @override   RenderObject createRenderObject(BuildContext context) {     return RenderCustomScrollbar(       controller: controller,       strokeWidth: strokeWidth ?? 16,       padding:           padding ?? const EdgeInsets.symmetric(             vertical: 16,              horizontal: 8,           ),       trackColor: trackColor ?? Colors.purpleAccent.withOpacity(0.3),       thumbColor: thumbColor ?? Colors.purpleAccent,     );   }    @override   void updateRenderObject(       BuildContext context, covariant RenderCustomScrollbar renderObject) {     if (strokeWidth != null) {       renderObject.strokeWidth = strokeWidth!;     }     if (padding != null) {       renderObject.padding = padding!;     }     if (trackColor != null) {       renderObject.trackColor = trackColor!;     }     if (thumbColor != null) {       renderObject.thumbColor = thumbColor!;     }   } }<\/code><\/pre>\n<p>Since our scrollbar has one child widget, that is why <em>CustomScrollbar<\/em> extends <em>SingleChildRenderObjectWidget<\/em> and override the <em>updateRenderObject()<\/em> and <em>createRenderObject()<\/em> methods to return a new instance of <em>RenderCustomScrollbar<\/em>.<\/p>\n<p>Now let&#8217;s move on to describing the <em>RenderShiftedBox<\/em> itself.<\/p>\n<p>Initialize the constructor and define a listener that will trigger the method to update the slider position, don&#8217;t forget to include <em>markNeedsPaint()<\/em> and <em>markNeedsSemanticsUpdate()<\/em> in that method so that the slider can be redrawn.<\/p>\n<p>\u00a0You can rewrite the methods for calculating the position, the height of the slider as you need, don&#8217;t forget to take into account the paddings, in general, you can make the runner move even diagonally. In my case, the height of the runner depends on the height of the content, in your case, the height of the slider can be fixed or even change dynamically, here you are not limited to anything. I&#8217;ve looked at the calculation of the position of the slider in the standard <em>RawScrollbar<\/em> and adapted it a little bit.<\/p>\n<p>Now let&#8217;s go to the very important function <em>performLayout()<\/em>:<\/p>\n<pre><code class=\"dart\">@override   void performLayout() {     size = constraints.biggest;     if (child == null) return;     child!.layout(constraints.copyWith(maxWidth: _getChildMaxWidth()),         parentUsesSize: !constraints.isTight);     final BoxParentData childParentData = child!.parentData! as BoxParentData;     childParentData.offset = Offset.zero;   }    double _getChildMaxWidth() {     return constraints.maxWidth - padding.horizontal - strokeWidth;   }<\/code><\/pre>\n<p>You need to override the <em>performLayout()<\/em> method and set a size for the <em>RenderBox<\/em>, otherwise, an exception will be thrown that the <em>RenderBox<\/em> hasn&#8217;t set its dimensions. You can take the size from constraints and then override the size of the child widget and its position, from where it starts to be drawn. Here you can also change the size or position of the child widget if you want. In my case <em>Offset.zero<\/em> because I only needed to limit the width of the child widget.<\/p>\n<p>After you&#8217;ve got all positions calculated according to paddings and any other optional parameters, you can start drawing, to do that we need to override the <em>paint()<\/em> method.<\/p>\n<pre><code class=\"dart\">@override   void paint(PaintingContext context, Offset offset) {     if (child == null) return;     context.paintChild(child!, offset);     _resetThumbStartPointIfNeeded();     _trackPaint(context, offset);     _thumbPaint(context, offset);     _textPaint(context, offset);   }<\/code><\/pre>\n<p>The <em>context.paintChild()<\/em> will draw the child widget, it already knows its dimensions. Then we draw the track, the slider, and the text in turn. There&#8217;s no need to go into detail about these methods in this article, you can read the code at this <a href=\"https:\/\/github.com\/safchemist\/custom_scrollbar\" rel=\"noopener noreferrer nofollow\">link<\/a>. In general, in the paint method, you can draw whatever you need.\u00a0<\/p>\n<h2>Result<\/h2>\n<div class=\"tm-iframe_temp\" data-src=\"https:\/\/embedd.srv.habr.com\/iframe\/611fac58920526a288c7e1db\" data-style=\"\" id=\"611fac58920526a288c7e1db\" width=\"\"><\/div>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/574296\/\"> https:\/\/habr.com\/ru\/articles\/574296\/<\/a><br \/><\/br><\/br><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-390364","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/390364","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=390364"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/390364\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=390364"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=390364"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=390364"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}