UnityTech / UIWidgets

UIWidget is a Unity Package which helps developers to create, debug and deploy efficient, cross-platform Apps.
Other
1.97k stars 256 forks source link

GestureDetector report onScaleUpdate with unexpected value in UIWidgetsEditorWindow #199

Closed JustinFincher closed 5 years ago

JustinFincher commented 5 years ago

Expected Behaviour

According to flutter/issues/13101, onScaleUpdate should report scale = 1.0 when there is only one pointer down on the screen, which also known as a pan or drag gesture.

Current Behaviour

In UIWidgetsPanel the GestureDetector works well. In UIWidgetsEditorWindow the GestureDetector reports a value that is not 1.0 when dragging around. (Unity 2018.4.0, macOS 10.14.5)

Test Script

ScrollView.cs

```csharp using System; using System.Collections.Generic; using FinGameWorks.Scripts.Helpers; using Unity.UIWidgets.foundation; using Unity.UIWidgets.material; using Unity.UIWidgets.painting; using Unity.UIWidgets.ui; using Unity.UIWidgets.widgets; using UnityEngine; using Transform = Unity.UIWidgets.widgets.Transform; namespace FinGameWorks.Scripts.Views { public class ScrollView : StatefulWidget { public readonly Widget child; public float MinScale; public float MaxScale; public float ContentSizeWidth; public float ContentSizeHeight; public ScrollView(Widget child, float minScale = 0.5f, float maxScale = 3.0f, float contentSizeWidth = 2000, float contentSizeHeight = 2000, Key key = null) : base(key) { this.child = child; MinScale = minScale; MaxScale = maxScale; ContentSizeWidth = contentSizeWidth; ContentSizeHeight = contentSizeHeight; } public override State createState() { return new ScrollViewState(); } } public class ScrollViewState : SingleTickerProviderStateMixin { public Offset Offset = Offset.zero; public float Scale = 1; public Offset MoveVelocity = Offset.zero; public GlobalKey ContentViewContainerKey = GlobalKey.key(); private float aimedScale = 1; private Offset aimedOffset = Offset.zero; // private Offset scaleOrigin = Offset.zero; private Offset previousPointerPosition = Offset.zero; private TimeSpan previousTime = TimeSpan.Zero; private float previousScale = 1; public override void initState() { base.initState(); WidgetsBinding.instance.scheduleFrameCallback(FrameCallback); } private void FrameCallback(TimeSpan dur) { setState(() => { // Offset moveDampSpeed = Offset.zero; // Offset offsetDampSpeed = Offset.zero; // float scaleDampSpeed = 0; // MoveVelocity = MoveVelocity.DampTo(Offset.zero, ref moveDampSpeed, 0.5f); // aimedOffset += MoveVelocity / (1000.0f / (dur - previousTime).Milliseconds); // Offset = Offset.DampTo(aimedOffset, ref offsetDampSpeed,0.15f); // Scale = Mathf.SmoothDamp(Scale, aimedScale, ref scaleDampSpeed, 0.2f); // Scale = Mathf.Max(widget.MinScale, Scale); // Scale = Mathf.Min(widget.MaxScale, Scale); // float scaleDiff = Scale - previousScale; // aimedOffset -= (scaleOrigin - aimedOffset) * scaleDiff; // previousTime = dur; // previousScale = Scale; }); WidgetsBinding.instance.scheduleFrameCallback(FrameCallback); } public override Widget build(BuildContext context) { return new GestureDetector ( onScaleStart: details => { }, onScaleUpdate: details => { // MoveVelocity = Offset.zero; float scaleDiff = previousScale * (details.scale - 1); if (aimedScale >= widget.MaxScale && scaleDiff > 0) { scaleDiff = 0; } if (aimedScale <= widget.MinScale && scaleDiff < 0) { scaleDiff = 0; } aimedScale += scaleDiff; aimedScale = Mathf.Max(widget.MinScale, aimedScale); aimedScale = Mathf.Min(widget.MaxScale, aimedScale); Offset scaleOffsetDiff = (aimedOffset - previousPointerPosition) * (scaleDiff / aimedScale); aimedOffset += scaleOffsetDiff; previousScale = details.scale; Debug.Log("scaleDiff = " + scaleDiff); }, onScaleEnd: details => { // MoveVelocity = details.velocity.pixelsPerSecond; }, child: new Listener ( onPointerDown: evt => { previousPointerPosition = evt.position; }, onPointerMove: evt => { aimedOffset += evt.delta; previousPointerPosition = evt.position; }, onPointerScroll: evt => { float scaleDiff = evt.delta.dy / MediaQuery.of(context).size.height; if (aimedScale >= widget.MaxScale && scaleDiff > 0) { scaleDiff = 0; } if (aimedScale <= widget.MinScale && scaleDiff < 0) { scaleDiff = 0; } aimedOffset += (aimedOffset - evt.position) * (scaleDiff / aimedScale); aimedScale += scaleDiff; aimedScale = Mathf.Max(widget.MinScale, aimedScale); aimedScale = Mathf.Min(widget.MaxScale, aimedScale); previousScale = aimedScale; }, child:new ClipRect ( clipBehavior:Clip.hardEdge, child:new Stack ( children: new List { new Positioned ( left: aimedOffset.dx, top: aimedOffset.dy, child: Transform.scale ( scale: aimedScale, child: new Container( key:ContentViewContainerKey, width: widget.ContentSizeWidth, height: widget.ContentSizeHeight, child:widget.child ), alignment:Alignment.topLeft ) ) } ) ) ) ); } } } ```

GridPointView.cs

```csharp using System; using Unity.UIWidgets.foundation; using Unity.UIWidgets.material; using Unity.UIWidgets.ui; using Unity.UIWidgets.widgets; using UnityEngine; using Canvas = Unity.UIWidgets.ui.Canvas; using Color = Unity.UIWidgets.ui.Color; namespace FinGameWorks.Scripts.Views { public class GridPointViewPainter : AbstractCustomPainter { private readonly float radius = 2f; private readonly float spacing = 60.0f; private Color color = Color.white; public GridPointViewPainter(float radius = 2, float spacing = 60.0f, Color color = null, Listenable repaint = null) : base(repaint) { this.radius = radius; this.spacing = spacing; this.color = color == null ? Colors.white : color; } public override void paint(Canvas canvas, Size size) { Path path = new Path(); for (int x = 1; x < size.width / spacing; x++) { for (int y = 1; y < size.height / spacing; y++) { path.addCircle(x * spacing,y * spacing, radius); } } Paint paint = new Paint { strokeWidth = 0, color = color, strokeCap = StrokeCap.round, strokeJoin = StrokeJoin.round, style = PaintingStyle.fill }; canvas.drawPath(path,paint); } public override bool shouldRepaint(CustomPainter oldDelegate) { return true; } } public class GridPointView : StatefulWidget { public Widget Child; public readonly float Radius; public readonly float Spacing; public readonly float Opacity; public GridPointView(float radius = 2, float spacing = 60.0f, float opacity = 0.1f, Widget child = null, Key key = null) : base(key) { Child = child; Radius = radius; Spacing = spacing; Opacity = opacity; } public override State createState() { return new GridPointViewState(); } } public class GridPointViewState : State { public override Widget build(BuildContext context) { return new CustomPaint ( painter: new GridPointViewPainter ( widget.Radius, widget.Spacing, Theme.of(context).dividerColor.withOpacity(widget.Opacity) ), isComplex: true, willChange: true, child: widget.Child ); } } } ```

AppMainScreen.cs

```csharp using System.Collections.Generic; using Unity.UIWidgets.foundation; using Unity.UIWidgets.material; using Unity.UIWidgets.widgets; using UnityEngine; namespace FinGameWorks.Scripts.Views { public class AppMainScreen : StatefulWidget { public override State createState() { return new AppMainScreenState(); } } class AppMainScreenState : State { public override Widget build(BuildContext context) { return new Scaffold ( body: new SafeArea ( child: new ScrollView(child: new GridPointView(1.5f, 35, 0.2f)) ) ); } } } ```

(Or we can transfer the whole project on WeChat)

Usage

Notice the output of Debug.Log("scaleDiff = " + scaleDiff);

JustinFincher commented 5 years ago

After exchanging the level of GestureDetector and Listener the issue was solved.

kgdev commented 5 years ago

is it a problem or not?

JustinFincher commented 5 years ago

@kgdev I believe this is not a bug, but rather an unexpected combine usage of Listener and GestureDetector widgets.
It seems if I add a GestureDetector as the parent of a Listener, the Listener's onPointerDown callback would report 2 pointers when there is actually only one pointer. So the GestureDetector would think there is a two-pointer scale gesture going on, thus reporting a scale value that is not 1.0.
If I make Listener the parent of the GestureDetector the issue would disappear.

JustinFincher commented 5 years ago

@kgdev It seems the issue was back 😂. Even if levels of the Listener and the GestureDetector were exchanged, there were still certain scenarios in which the scale value is not equal to 1, and this issue only happens in UIWidgetsEditorWindow, not in UIWidgetsPanel.

Updated ScrollView.cs

using System;
using System.Collections.Generic;
using FinGameWorks.Scripts.Helpers;
using Unity.UIWidgets.foundation;
using Unity.UIWidgets.material;
using Unity.UIWidgets.painting;
using Unity.UIWidgets.ui;
using Unity.UIWidgets.widgets;
using UnityEngine;
using Transform = Unity.UIWidgets.widgets.Transform;

namespace FinGameWorks.Scripts.Views
{
    public class ScrollView : StatefulWidget
    {
        public readonly Widget child;
        public float MinScale;
        public float MaxScale;
        public float ContentSizeWidth;
        public float ContentSizeHeight;

        public ScrollView(Widget child, float minScale = 0.5f, float maxScale = 3.0f, float contentSizeWidth = 2000, float contentSizeHeight = 2000, Key key = null) : base(key)
        {
            this.child = child;
            MinScale = minScale;
            MaxScale = maxScale;
            ContentSizeWidth = contentSizeWidth;
            ContentSizeHeight = contentSizeHeight;
        }

        public override State createState()
        {
            return new ScrollViewState();
        }
    }

    public class ScrollViewState : SingleTickerProviderStateMixin<ScrollView>
    {
        public Offset Offset = Offset.zero;
        public float Scale = 1;
        public Offset MoveVelocity = Offset.zero;
        public GlobalKey ContentViewContainerKey = GlobalKey.key();

        private float aimedScale = 1;
        private Offset aimedOffset = Offset.zero;
//        private Offset scaleOrigin = Offset.zero;
        private Offset previousPointerPosition = Offset.zero;
        private TimeSpan previousTime = TimeSpan.Zero;
        private float previousScale = 1;

        public override void initState()
        {
            base.initState();
            WidgetsBinding.instance.scheduleFrameCallback(FrameCallback);
        }

        private void FrameCallback(TimeSpan dur)
        {
            setState(() =>
            {
//                Offset moveDampSpeed = Offset.zero;
//                Offset offsetDampSpeed = Offset.zero;
//                float scaleDampSpeed = 0;

//                MoveVelocity = MoveVelocity.DampTo(Offset.zero, ref moveDampSpeed, 0.5f);
//                aimedOffset += MoveVelocity / (1000.0f / (dur - previousTime).Milliseconds);
//                Offset = Offset.DampTo(aimedOffset, ref offsetDampSpeed,0.15f);
//                Scale = Mathf.SmoothDamp(Scale, aimedScale, ref scaleDampSpeed, 0.2f);
//                Scale = Mathf.Max(widget.MinScale, Scale);
//                Scale = Mathf.Min(widget.MaxScale, Scale);
//                float scaleDiff = Scale - previousScale;
//                aimedOffset -= (scaleOrigin - aimedOffset) * scaleDiff;

//                previousTime = dur;
//                previousScale = Scale;
            });
            WidgetsBinding.instance.scheduleFrameCallback(FrameCallback);
        }

        public override Widget build(BuildContext context)
        {
            return new Listener
            (
                onPointerDown: evt =>
                {
                    previousPointerPosition = evt.position;
                },
                onPointerMove: evt =>
                {
                    aimedOffset += evt.delta;
                    previousPointerPosition = evt.position;
                    if (aimedOffset.dx > 0)
                    {
                        aimedOffset = new Offset(0, aimedOffset.dy);
                    }
                    if (aimedOffset.dy > 0)
                    {
                        aimedOffset = new Offset(aimedOffset.dx, 0);
                    }
                    if (widget.ContentSizeWidth * aimedScale + aimedOffset.dx < MediaQuery.of(context).size.width)
                    {
                        aimedOffset = new Offset(MediaQuery.of(context).size.width - widget.ContentSizeWidth * aimedScale, aimedOffset.dy);
                    }
                    if (widget.ContentSizeHeight * aimedScale + aimedOffset.dy < MediaQuery.of(context).size.height)
                    {
                        aimedOffset = new Offset( aimedOffset.dx, MediaQuery.of(context).size.height - widget.ContentSizeHeight * aimedScale);
                    }
                },
                onPointerScroll: evt =>
                {
                    float scaleDiff = evt.delta.dy / MediaQuery.of(context).size.height;
                    if (aimedScale >= widget.MaxScale && scaleDiff > 0)
                    {
                        scaleDiff = 0;
                    }

                    if (aimedScale <= widget.MinScale && scaleDiff < 0)
                    {
                        scaleDiff = 0;
                    }

                    aimedOffset += (aimedOffset - evt.position) * (scaleDiff / aimedScale);
                    aimedScale += scaleDiff;
                    aimedScale = Mathf.Max(widget.MinScale, aimedScale);
                    aimedScale = Mathf.Min(widget.MaxScale, aimedScale);
                    previousScale = aimedScale;
                },
                child: new GestureDetector
                (
                    onScaleStart: details => { },
                    onScaleUpdate: details =>
                    {
//                    MoveVelocity = Offset.zero;

                        float scaleDiff = previousScale * (details.scale - 1);
                        if (aimedScale >= widget.MaxScale && scaleDiff > 0)
                        {
                            scaleDiff = 0;
                        }

                        if (aimedScale <= widget.MinScale && scaleDiff < 0)
                        {
                            scaleDiff = 0;
                        }

                        aimedScale += scaleDiff;
                        if (widget.ContentSizeWidth * aimedScale < MediaQuery.of(context).size.width || widget.ContentSizeHeight * aimedScale < MediaQuery.of(context).size.height)
                        {
                            aimedScale = Mathf.Min(MediaQuery.of(context).size.width / widget.ContentSizeWidth,
                                MediaQuery.of(context).size.height / widget.ContentSizeHeight);
                        }
                        aimedScale = Mathf.Max(widget.MinScale, aimedScale);
                        aimedScale = Mathf.Min(widget.MaxScale, aimedScale);

                        Offset scaleOffsetDiff = (aimedOffset - previousPointerPosition) * (scaleDiff / aimedScale);
                        aimedOffset += scaleOffsetDiff;
                        previousScale = details.scale;
                        Debug.Log(scaleDiff);
                    },
                    onScaleEnd: details =>
                    {
//                    MoveVelocity = details.velocity.pixelsPerSecond;
                    },
                    child: new ClipRect
                    (
                        clipBehavior: Clip.hardEdge,
                        child: new Stack
                        (
                            children: new List<Widget>
                            {
                                new Positioned
                                (
                                    left: aimedOffset.dx,
                                    top: aimedOffset.dy,
                                    child: Transform.scale
                                    (
                                        scale: aimedScale,
                                        child: new Container(
                                            key: ContentViewContainerKey,
                                            width: widget.ContentSizeWidth,
                                            height: widget.ContentSizeHeight,
                                            child: widget.child
                                        ),
                                        alignment: Alignment.topLeft
                                    )
                                )
                            }
                        )
                    )
                )
            );
        }
    }
}