Xamarin Formsのマウスイベントが少ない話の続き

yarukizero.hatenablog.jp

前回書かなかったレンダラの実装。今回は実際に書いたコードを張り付けるだけの簡単なお仕事なので前回の話と微妙に違う。

まず共通部分。

public interface IClickableView {
	bool CallClick();
	bool CallPointerDown();
	bool CallPointerUp();
}

public class ClickableView : global::Xamarin.Forms.BoxView, IClickableView {
	public static readonly BindableProperty IsPointerDownProperty =
		BindableProperty.Create(nameof(IsPointerDown), typeof(bool), typeof(ClickableView), default(bool),
			propertyChanged: (bindable, oldValue, newValue) => ((ClickableView)bindable).IsPointerDown = (bool)newValue);

	public bool IsPointerDown {
		get { return (bool)GetValue(IsPointerDownProperty); }
		set { SetValue(IsPointerDownProperty, value); }
	}
	
	public static readonly BindableProperty ForegroundColorProperty =
		BindableProperty.Create(nameof(ForegroundColor), typeof(Color), typeof(ClickableView), default(Color),
			propertyChanged: (bindable, oldValue, newValue) =>	((ClickableView)bindable).ForegroundColor = (Color)newValue);

	public Color ForegroundColor {
		get { return (Color)GetValue(ForegroundColorProperty); }
		set { SetValue(ForegroundColorProperty, value); }
	}
	
	public event EventHandler<EventArgs> Clicked;
	public event EventHandler<EventArgs> PointerDown;
	public event EventHandler<EventArgs> PointerUp;

	bool IClickableView.CallClick() {
		Clicked?.Invoke(this, EventArgs.Empty);

		return Clicked != null;
	}

	bool IClickableView.CallPointerDown() {
		IsPointerDown = true;
		PointerDown?.Invoke(this, EventArgs.Empty);

		return PointerDown != null;
	}
	bool IClickableView.CallPointerUp() {
		IsPointerDown = false;
		PointerUp?.Invoke(this, EventArgs.Empty);

		return PointerUp != null;
	}
}

プラットフォーム個別実装でandroid

public class ClickableViewHelper {
    public class Helper<T> where T : Android.Views.View, IVisualElementRenderer {
        private IClickableView target;

        public Helper(T renderer) {
            renderer.ElementChanged += (s, e) =>
            {
                if (e.NewElement == null) {
                    // 後片付け必要?
                    return;
                } else { 
                    target = e.NewElement as IClickableView;
                    System.Diagnostics.Debug.Assert(target != null);
                    renderer.Touch += (snder, ev) =>
                    {
                        var pointerIndex = ((int)ev.Event.Action) >> ((int)MotionEventActions.PointerIndexShift) & ((int)MotionEventActions.PointerIndexMask);
                        switch (ev.Event.Action & MotionEventActions.Mask) {
                            case MotionEventActions.Down:
                                ev.Handled = target.CallPointerDown();
                                break;
                            case MotionEventActions.Up:
                                ev.Handled = target.CallPointerUp();
                                if(new Rectangle(
                                    0, 0,
                                    renderer.Width, renderer.Height).Contains(
                                        ev.Event.GetX(), ev.Event.GetY())) {

                                    target.CallClick();
                                }
                                break;

                            case MotionEventActions.Cancel:
                                break;

                        }
                    };
                }
            };
        }
    }

    public static Helper<T> Create<T>(T renderer) where T : Android.Views.View, IVisualElementRenderer {
        return new Helper<T>(renderer);
    }
}

public class ClickableViewRenderer : BoxRenderer {
    private object helper;
    public ClickableViewRenderer() {
        helper = ClickableViewHelper.Create(this);
    }
}

UWPはこちら。

public class ClickableViewHelper {
    public class Helper<T> where T : Windows.UI.Xaml.Controls.Panel, IVisualElementRenderer {
        private IClickableView target;

        public Helper(T renderer) {
            renderer.ElementChanged += (s, e) =>
            {
                if (e.NewElement == null) {
                    // 後片付け必要?

                    return;
                } else {
                    target = e.NewElement as IClickableView;
                    System.Diagnostics.Debug.Assert(target != null);

                    renderer.PointerPressed += (sender, ev) =>
                    {
                        renderer.CapturePointer(ev.Pointer);
                        target?.CallPointerDown();
                    };
                    renderer.PointerReleased += (sender, ev) =>
                    {
                        renderer.ReleasePointerCapture(ev.Pointer);
                        if (target != null) {
                            target.CallPointerUp();

                            var pt = ev.GetCurrentPoint(renderer);
                            if ((0 <= pt.Position.X)
                                && (pt.Position.X <= renderer.ActualWidth)
                                && (0 <= pt.Position.Y)
                                && (pt.Position.Y <= renderer.ActualHeight)) {

                                target.CallClick();
                            }
                        }
                    };
                }
            };
        }
    }

    public static Helper<T> Create<T>(T renderer) where T : Windows.UI.Xaml.Controls.Panel, IVisualElementRenderer {
        return new Helper<T>(renderer);
    }
}

public class ClickableViewRenderer : BoxViewRenderer {
    private object helper;
    public ClickableViewRenderer() {
        helper = ClickableViewHelper.Create(this);
    }
}

Xamarin Formsのマウスイベントが少ない話

この文章はローカルで書きためておいたので現状すでに乖離している可能性があります。

ここ最近xamarinを使用しているのだけどプラットフォーム互換が障害になっているのかマウス/タッチイベント系がなくて困る。MouseDown/Upとかそういうの。ないものはないので仕方がないわけで自作しようとかそんな話。

ということでBoxViewを継承して下記みたいなViewを作る。UWPをサポートする都合上Pointerという一番抽象度が高いかなとか思うUWPの名前にした。対応するレンダラはそのまま素直に作ればよいので特に問題は出ないと思う。PointerDown/Upイベントがないとかは用意したけど今回使っていないし、なくても困らない気もするので書いていない。

 public class ClickablieView : BoxView {
     public static readonly BindableProperty IsPointerDownProperty;
     public static readonly BindableProperty ForegroundColorProperty;

     public bool IsPointerDown { get; set; }
     public Color ForegroundColor { get; set; }

     public event EventHandler<EventArgs> Clicked;
 }

できたところでXAMLをかく。作ったClickableViewを下に、上にLabelを載せることでボタンぽい見た目としている。定義したForegroundColorはそのまま使用するのではなく、ボタンのテキスト色としてバインドする用途に使う。

 <!-- ボタンのコンテナ -->
 <Grid WidthRequest="128" HeightRequest="48">
  <c:ClickableView x:Name="button" ForegroundColor="White" >
    <View.Triggers>
      <Trigger TargetType="c:ClickableView" Property="IsPointerDown" Value="True">
        <Setter Property="BackgroundColor" Value="Black" />
        <Setter Property="ForegroundColor" Value="Red" />
      </Trigger>
    </View.Triggers>
    <View.Behaviors>
      <i:EventToCommand 
          EventName="Clicked"
          Command="{Binding ButtonClickCommand}" />
    </View.Behaviors>
  </c:ClickableView>
  <Label
    Style="{StaticResource ButtonTextStyle}" 
    Text="新規" 
    TextColor="{Binding Source={x:Reference button},Path=ForegroundColor}" />
 </Grid>

で、androidはこれで特に問題がないのだけどUWPはLabelがマウス処理イベントを奪うという問題に直面する。なんでそんなことになっているのか(UWPのデフォがそうなのか、xamarinがなにかやっているのか)調べてないけど、困るので対策を入れる。下記、

 public class AzLabelRenderer : LabelRenderer {
     private Reactive.Bindings.ReactiveProperty<bool> t;
     
     protected override void OnElementChanged(ElementChangedEventArgs<Label> e) {
         base.OnElementChanged(e);
         
         t = new Reactive.Bindings.ReactiveProperty<bool>();
         t.PropertyChanged += (s, ev) =>
         {
             if (t.Value) {
                 t.Value = false;
             }
         };
         
         data.BindingOperations.SetBinding(this,
             Windows.UI.Xaml.Controls.TextBlock.IsHitTestVisibleProperty,
             new data.Binding()
                 {
                     Path = new Windows.UI.Xaml.PropertyPath("Value"),
                     Source = t,
                     Mode = data.BindingMode.TwoWay,
                 });
         this.IsHitTestVisible = false;
     }
 }

IsHitTestが常にfalseとなるように細工する。特にReactivePropertyを使う必要はないのだけど、よそで使っているのにこれだけのためにクラス作るのもめどいので利用する。ビバ怠惰。

長くなったのでおわる。