言いたいことはそれだけか

KotlinとかAndroidとかが好きです。調べたことをメモします。٩( 'ω' )و

便利なTextInputLayoutとその内部実装

はじめに

Material Designにいつの間にか入っていたTextInputLayoutが便利なのでメモを残す。

TextInputLayout とは

公式ドキュメント によると、TextInputLayout とは「テキストを入力する時にhintを隠す代わりにfloating labelを表示するEditTextをラップしたview」らしい。 これだけ読んでも謎だと思うので、Google Code LabのMaterial Design Componentsのコースをgit cloneしてきて動かしてみるとわかりやすいです。 あとは最近WebのGoogle Login画面でこのcomponentが使われている気がする。

MDC-104 Android: Material Advanced Components (Kotlin)

なんかgifがうまくアップロードできなくてanimation部分が見えない…

f:id:muumuumuumuu:20190104182847g:plain:w200

ちなみに上記はstyleにWidget.MaterialComponents.TextInputLayout.OutlineBoxが設定されている。

どうやってhintを動かしているのか?

せっかくなのでこのhintのanimationがどのように実装されているのかコードを読んでみた。 ちなみに上記のCode Labからgit cloneしてきたプロジェクトだとTextInputLayout はAndroidXではなく com.android.support:support-v4:28.0.0-alpha3 だったので、これを読んでいく。

animationしているのは下記のメソッド。このメソッドの引数のfloatには、collapseの場合は1.0F, expandの時は0.0Fが渡される。

    @VisibleForTesting
    void animateToExpansionFraction(float target) {
        if (this.collapsingTextHelper.getExpansionFraction() != target) {
            if (this.animator == null) {
                this.animator = new ValueAnimator();
                this.animator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
                this.animator.setDuration(167L);
                this.animator.addUpdateListener(new AnimatorUpdateListener() {
                    public void onAnimationUpdate(ValueAnimator animator) {
                        TextInputLayout.this.collapsingTextHelper.setExpansionFraction((Float)animator.getAnimatedValue());
                    }
                });
            }

            this.animator.setFloatValues(new float[]{this.collapsingTextHelper.getExpansionFraction(), target});
            this.animator.start();
        }
    }

ValueAnimator を使ってanimationしている。Interpolatorを設定して167Lのdurationをセット。(なんで167に決まったんだろう?)
で現在のexpansion fractionをHelperクラスからとってきて、そこから引数のtargetまでをvalueの変更幅に指定する。

start()を読んでやるとlistenerに設定したcallback methodが呼ばれる。

実際にviewをanimationするのはまたもやhelperクラスの方に移譲している。

    public void setExpansionFraction(float fraction) {
        fraction = MathUtils.clamp(fraction, 0.0F, 1.0F);
        if (fraction != this.expandedFraction) {
            this.expandedFraction = fraction;
            this.calculateCurrentOffsets();
        }

    }
    private void calculateOffsets(float fraction) {
        this.interpolateBounds(fraction);
        this.currentDrawX = lerp(this.expandedDrawX, this.collapsedDrawX, fraction, this.positionInterpolator);
        this.currentDrawY = lerp(this.expandedDrawY, this.collapsedDrawY, fraction, this.positionInterpolator);
        this.setInterpolatedTextSize(lerp(this.expandedTextSize, this.collapsedTextSize, fraction, this.textSizeInterpolator));
        if (this.collapsedTextColor != this.expandedTextColor) {
            this.textPaint.setColor(blendColors(this.getCurrentExpandedTextColor(), this.getCurrentCollapsedTextColor(), fraction));
        } else {
            this.textPaint.setColor(this.getCurrentCollapsedTextColor());
        }

        this.textPaint.setShadowLayer(lerp(this.expandedShadowRadius, this.collapsedShadowRadius, fraction, (TimeInterpolator)null), lerp(this.expandedShadowDx, this.collapsedShadowDx, fraction, (TimeInterpolator)null), lerp(this.expandedShadowDy, this.collapsedShadowDy, fraction, (TimeInterpolator)null), blendColors(this.expandedShadowColor, this.collapsedShadowColor, fraction));
        ViewCompat.postInvalidateOnAnimation(this.view);
    }

currentDrawXcurrentDrawYそのほか諸々の計算を済ませておいてpostInvalidateOnAnimation()をコールして描画し直す時にその値を使う感じっぽい。 ViewCompat.postInvalidateOnAnimation() を辿っていくとView#invalidate() が呼ばれていることがわかる。

postInvalidateOnAnimation() の引数にはTextInputLayoutが渡されているので、このクラスのdraw()をみてみるとhelperクラスのdraw()が呼ばれている。

    public void draw(Canvas canvas) {
        if (this.boxBackground != null) {
            this.boxBackground.draw(canvas);
        }

        super.draw(canvas);
        if (this.hintEnabled) {
            this.collapsingTextHelper.draw(canvas);
        }

    }

この中でcalculateOffsets() で計算したx,yの値やsetInterpolatedTextSize()の内部で計算・設定されているscaleを使ってviewを動かしている。 動かすのはcanvasに対してscale()とかdrawBitmap()/drawText()とかをコールして実装している。
(あまり関係ないけどvar10000とかvar7の変数はなんのために作られたのかわからない…)

    public void draw(Canvas canvas) {
        int saveCount = canvas.save();
        if (this.textToDraw != null && this.drawTitle) {
            float x = this.currentDrawX;
            float y = this.currentDrawY;
            boolean drawTexture = this.useTexture && this.expandedTitleTexture != null;
            float ascent;
            if (drawTexture) {
                ascent = this.textureAscent * this.scale;
                float var10000 = this.textureDescent * this.scale;
            } else {
                ascent = this.textPaint.ascent() * this.scale;
                float var7 = this.textPaint.descent() * this.scale;
            }

            if (drawTexture) {
                y += ascent;
            }

            if (this.scale != 1.0F) {
                canvas.scale(this.scale, this.scale, x, y);
            }

            if (drawTexture) {
                canvas.drawBitmap(this.expandedTitleTexture, x, y, this.texturePaint);
            } else {
                canvas.drawText(this.textToDraw, 0, this.textToDraw.length(), x, y, this.textPaint);
            }
        }

        canvas.restoreToCount(saveCount);
    }

アニメーションが早すぎて見えないけどTextColorもアニメーションに合わせて徐々に変わるように設定されていて細かい。すごい。でも見えない。

    private static int blendColors(int color1, int color2, float ratio) {
        float inverseRatio = 1.0F - ratio;
        float a = (float)Color.alpha(color1) * inverseRatio + (float)Color.alpha(color2) * ratio;
        float r = (float)Color.red(color1) * inverseRatio + (float)Color.red(color2) * ratio;
        float g = (float)Color.green(color1) * inverseRatio + (float)Color.green(color2) * ratio;
        float b = (float)Color.blue(color1) * inverseRatio + (float)Color.blue(color2) * ratio;
        return Color.argb((int)a, (int)r, (int)g, (int)b);
    }