2017-07-25 78 views
0

我想實現一個水平滾動的價值選擇,與此類似: Scrollable tape value selector撲:如何創建一個自定義滾動部件

用戶滾動「磁帶」向左或向右,選擇價值(顯示在中間的框中)。該磁帶具有最大值和最小值,當達到該值時將顯示典型的過捲動畫(Android上發光; iOS上反彈)。

Hixie在Gitter上建議我可以使用GestureDetector + CustomPaint,但我有一種感覺,我必須自己實現滾動邏輯,並且不會利用Flutter的fling和overscroll實現。

編輯:在進一步調查之後,我改變了原來的做法使用低級別的小部件,如ScrollableViewport即得。

我已經能夠創建磁帶通過擴展CustomPaint,其寬度設置爲帶全長: _width = (_maxValue - _minValue) * _spacing;

然後,我把我的自定義窗口小部件CustomScrollView內:

import 'package:flutter/material.dart'; 
import 'package:flutter/rendering.dart'; 

void main() { 
    runApp(new MaterialApp(home: new Scaffold(
     appBar: new AppBar(title: new Text("Test"),), 
     body: new CustomScrollView(
     scrollDirection: Axis.horizontal, 
     slivers: <Widget>[ 
      new SliverToBoxAdapter(
      child: new Tape(), 
     ) 
     ], 
    ) 
))); 
} 

const _width = (_maxValue - _minValue) * spacing; 
const spacing = 20.0; 
const _minValue = 0; 
const _maxValue = 100; 

class Tape extends CustomPaint { 
    Tape() : super(
    size: new Size(_width, 60.0), 
    painter: new _TapePainter(), 
); 
} 

class _TapePainter extends CustomPainter { 
    Paint _tickPaint; 

    _TapePainter() { 
    _tickPaint = new Paint(); 
    _tickPaint.color = Colors.black; 
    _tickPaint.strokeWidth = 1.0; 
    } 

    @override 
    void paint(Canvas canvas, Size size) { 
    var rect = Offset.zero & size; 

    var o1 = new Offset(0.0, 0.0); 
    var o2 = new Offset(0.0, rect.height); 

    while (o1.dx < size.width) { 
     canvas.drawLine(o1, o2, _tickPaint); 
     o1 = o1.translate(spacing, 0.0); 
     o2 = o2.translate(spacing, 0.0); 
    } 
    } 

    @override 
    bool shouldRepaint(_TapePainter oldDelegate) { 
    return true; 
    } 
} 

這實現了我想要的效果:我現在可以左右滾動錄像帶,並免費獲得超滾動效果。

問題是,當前的代碼效率低下:整個磁帶被繪製一次,滾動只是簡單地移動緩衝的位圖。這對非常大的「磁帶」造成問題。

相反,我正在尋找的是在每個框架上重新繪製小部件,以便只需要計算和繪製可見部分。這也將允許我實現其他滾動相關效果,例如,在接近中心時動態地衰減數字。

回答

1

因此經過相當多的調查後,我設法解決了這個問題。我很確定我的解決方案不是最好的方式來做到這一點,但它的工作原理。如果有人能評論解決方案的質量和如何改進,我將不勝感激。

我複製了SliverBoxAdapter的代碼,返回一個自定義版本RenderSliverToBoxAdapter,它在每個佈局過程中公開可見幾何圖形(實際可見的小部件部分)。 然後,我的CustomPainter使用此信息將繪圖命令限制爲僅出現在可見區域內的繪圖命令。

請注意,以下代碼僅用作概念證明,因此很難理解。我將它延伸到這裏一個完整的解決方案:https://github.com/cachapa/FlutterTapeSelector

import 'package:flutter/material.dart'; 
import 'package:flutter/rendering.dart'; 

void main() { 
    runApp(new MaterialApp(
     home: new Scaffold(
      appBar: new AppBar(
      title: new Text("Test"), 
     ), 
      body: new CustomScrollView(
      scrollDirection: Axis.horizontal, 
      slivers: <Widget>[ 
       new CustomSliverToBoxAdapter(
       child: new Tape(), 
      ) 
      ], 
     )))); 
} 

const _width = (_maxValue - _minValue) * spacing; 
const spacing = 20.0; 
const _minValue = 0; 
const _maxValue = 100; 

class Tape extends CustomPaint { 
    Tape() 
     : super(
      size: new Size(_width, 60.0), 
      painter: new _TapePainter(), 
     ); 
} 

class _TapePainter extends CustomPainter { 
    Paint _tickPaint = new Paint(); 

    _TapePainter() { 
    _tickPaint.color = Colors.black; 
    _tickPaint.strokeWidth = 2.0; 
    } 

    @override 
    void paint(Canvas canvas, Size size) { 
    var rect = Offset.zero & size; 

    // Extend drawing window to compensate for element sizes - avoids lines at either end "popping" into existence 
    var extend = _tickPaint.strokeWidth/2.0; 

    // Calculate from which Tick we should start drawing 
    var tick = ((_visibleRect.left - extend)/spacing).ceil(); 

    var startOffset = tick * spacing; 
    var o1 = new Offset(startOffset, 0.0); 
    var o2 = new Offset(startOffset, rect.height); 

    while (o1.dx < _visibleRect.right + extend) { 
     canvas.drawLine(o1, o2, _tickPaint); 
     o1 = o1.translate(spacing, 0.0); 
     o2 = o2.translate(spacing, 0.0); 
    } 
    } 

    @override 
    bool shouldRepaint(_TapePainter oldDelegate) { 
    return false; 
    } 
} 

class CustomSliverToBoxAdapter extends SingleChildRenderObjectWidget { 
    const CustomSliverToBoxAdapter({ 
    Key key, 
    Widget child, 
    }) 
     : super(key: key, child: child); 

    @override 
    CustomRenderSliverToBoxAdapter createRenderObject(BuildContext context) => 
     new CustomRenderSliverToBoxAdapter(); 
} 

class CustomRenderSliverToBoxAdapter extends RenderSliverSingleBoxAdapter { 
    CustomRenderSliverToBoxAdapter({ 
    RenderBox child, 
    }) 
     : super(child: child); 

    @override 
    void performLayout() { 
    if (child == null) { 
     geometry = SliverGeometry.zero; 
     return; 
    } 
    child.layout(constraints.asBoxConstraints(), parentUsesSize: true); 
    double childExtent; 
    switch (constraints.axis) { 
     case Axis.horizontal: 
     childExtent = child.size.width; 
     break; 
     case Axis.vertical: 
     childExtent = child.size.height; 
     break; 
    } 
    assert(childExtent != null); 
    final double paintedChildSize = 
     calculatePaintOffset(constraints, from: 0.0, to: childExtent); 
    assert(paintedChildSize.isFinite); 
    assert(paintedChildSize >= 0.0); 
    geometry = new SliverGeometry(
     scrollExtent: childExtent, 
     paintExtent: paintedChildSize, 
     maxPaintExtent: childExtent, 
     hitTestExtent: paintedChildSize, 
     hasVisualOverflow: childExtent > constraints.remainingPaintExtent || 
      constraints.scrollOffset > 0.0, 
    ); 
    setChildParentData(child, constraints, geometry); 

    // Expose geometry 
    _visibleRect = new Rect.fromLTWH(
     constraints.scrollOffset, 0.0, geometry.paintExtent, child.size.height); 
    } 
} 

Rect _visibleRect = Rect.zero; 
0

說實話我並不是很瞭解你的問題,但最近我開發了NumberPicker。也許它會以某種方式幫助你。 https://github.com/MarcinusX/NumberPicker

+0

這是一個類似的目標,但你的實現是基於管理所有的圖紙給你一個ListView。 我更願意自己做繪畫,同時利用現有的Flutter滾動實現。 – Cachapa

+0

這就是我想的:(嗯,請分享代碼或github鏈接,當你將處理這個。我很樂意看到它在你的方式工作:) –

+0

會做。該計劃是將其作爲通用小部件發佈。 – Cachapa