便利な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部分が見えない…
ちなみに上記は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); }
currentDrawX
やcurrentDrawY
そのほか諸々の計算を済ませておいて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); }