kamulog

xamarin.formsのネタなど

Xamarin.Forms(Android)でコードだけで任意のViewにRippleEffectを追加する方法

C#のコードだけで任意のViewにRippleEffectを追加する方法をご紹介します。

Xamarin.Androidであれば別にxmlで書けば何の問題も無いと思うんですが(検索したらその方法はたくさんヒットするし)、FormsからCustomRendererやEffectsで使用するとなるとコードで書けた方が何かと都合が良いので調べてみました。

今回調べた方法は、実際にこちらのCommandを追加するEffectで使っています。

kamusoft.hatenablog.jp

使用部分のコード github.com

基本的な手順

  1. RippleEffectの色をColorStateList型で作成
  2. 対象viewのbackgroundがnullの場合はマスクするDrawableを作成
    • nullでなければbackgroundをどこかに退避
  3. 1と2を指定してRippleDrawableを作成
  4. 3を対象viewのbackgroundにセット

ColorStateListの作り方

こういう関数を用意しときます。これで希望の色を渡してあげればOKです。

ColorStateList getPressedColorSelector(int pressedColor)
{
    return new ColorStateList(
        new int[][]
        {
            new int[]{}
        },
        new int[]
        {
            pressedColor,
        });
}

How to use RippleDrawable programmatically in code (not xml) with Android 5.0 Lollipop? - Stack Overflow

Mask用のDrawableの作り方

ColorDrawableを普通に作ればOKです。

var mask = new ColorDrawable(Android.Graphics.Color.White);

backgroundが指定されていない場合はこのマスク用DrawableをセットしないとRippleが発動しないのでその対策です。 何故なのかはすみません、わかりません…。

Android 5.0のRippleDrawable概論:既存アプリのRipple対応まで - Qiita

RippleDrawableの作り方

var ripple = new RippleDrawable(colorStateList ,background, maskDrawable);

1番目がColorStateList型でリプル色を指定、2番目が内包するDrawableを指定、3番目がマスクするDrawableを指定といった感じです。

上記を踏まえてBackgroundにRippleDrawableを指定

var back = _view.Background;
if (back == null) {
    //背景なし
    var mask = new ColorDrawable(Android.Graphics.Color.White);
    _view.Background = new RippleDrawable(getPressedColorSelector(color), null, mask);
}
else {
    //背景あり
    _view.Background = new RippleDrawable(getPressedColorSelector(color), back, null);
}

これで出来上がりです。 EditTextなんかは、そこかよ!ってところがRipple化してたりしますが、概ねどのviewでもいけるかと思います。

Layout系の問題点 オーバーレイできない

StackLayoutなどのLayout系も前項の方法でRippleEffectを追加できますが、その場合子要素には全く影響しません。

詳しくはこちらの記事に書かれていますが、Backgroundに追加するので効果が背面にしか起きないんですよね。

Material Design度が高まるRipple Effect対応 - Qiita

子要素も含めて効果を出したい場合はForegroundにRippleDrawableをセットしなくてはいけないんですが、ForegroundがあるコンテナはFrameLayoutくらいしかなくて、FormsのLayout系で使われているRendererはFrameLayoutでは無いのでForegroundにセットする方法が使えません。

Layout系でオーバーレイさせる代替案

Layout系に対応するNativeのViewGroupに、ForegroundにRippleDrawableをセットしたFrameLayoutをAddViewして対応します。

このViewGroupですが、謎のViewGroupでLinearLayoutとか既存のLayoutを継承しているワケじゃ無さそうなんですよね。 なので、LayoutOptionで目一杯と指定してもまったく無反応です。

それを何とかするためにOnLayoutChangeListenerを登録してコンテナのLayoutが変わるタイミングで子要素のサイズを設定するようにしています。 これはEffectsの場合の方法で、CustomRendererの場合ならOnLayoutをoverrideすれば良さそうです。

void hoge()
{
    var rippleOverlay = new FrameLayout(Container.Context);
    rippleOverlay.LayoutParameters = new ViewGroup.LayoutParams(-1, -1);

    var mask = new ColorDrawable(Android.Graphics.Color.White);
    rippleOverlay.Foreground = new RippleDrawable(getPressedColorSelector(color), null, mask);

    var rippleListener = new ContainerOnLayoutChangeListener(rippleOverlay);
    Container.AddOnLayoutChangeListener(rippleListener);

    Container.AddView(rippleOverlay);

    rippleOverlay.BringToFront();

}
//
internal class ContainerOnLayoutChangeListener : Java.Lang.Object, Android.Views.View.IOnLayoutChangeListener
{
    private Android.Widget.FrameLayout _layout;

    public ContainerOnLayoutChangeListener(Android.Widget.FrameLayout layout)
    {
        _layout = layout;
    }

    //ContainerにAddViewした子要素のサイズを確定する必要があるため
    //ContainerのOnLayoutChangeのタイミングでセットする
    public void OnLayoutChange(Android.Views.View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom)
    {
        _layout.Right = v.Width;
        _layout.Bottom = v.Height;
    }
}

f:id:kamusoft:20170126193215g:plain