Androidでのコンポーネントのサイズ変更のアニメーション化

Hi%username%! 今日は、Androidアプリケーションのコンポーネントのアニメートされたサイズ変更を追加の労力なしで実装する方法を皆さんと共有したいと思います。



私はアニメーションについて多くのことを読みましたが、それでもインターフェースでそれを使用することができませんでした。 私は最終的に、あらゆる種類のレイアウト遷移アニメーターレイアウトアニメーションを試し、このテーマに関する記事を書きたいと思いました。 ただし、カスタムViewGroupObjectAnimatorというはるかに散文的なものになりました。



そこで、次のように、Chrome for Androidのように、フォーカスを受け取ったときにEditTextを展開したかったのです。







StackOverflowをすばやくスクロールすると、おおよその移動方向を決定するための2つの実装オプションが見つかりました。



  1. ScaleAnimationを使用します
  2. いずれにせよ、ステップごとにEditTextのサイズを変更し、各ステップでrequestLayout()を要求します。


最初のオプションは、少なくとも文字が伸びるので、すぐに却下しました。 2番目のオプションは、各ステップがViewGroup全体でonMeasure / onLayout / onDrawサイクルを完全に実行することを除いて、はるかに論理的に聞こえますが、EditTextの表示を変更するだけです。 さらに、このようなアニメーションはまったく滑らかに見えないと思われました。



2番目のメソッドを基本として、すべてのステップでrequestLayout()を呼び出すことから逃れる方法を考え始めます。 しかし、予想どおり、小さなものから始めましょう。



ViewGroupを作成する



コンポーネントをホストするカスタムViewGroupを作成することから始めましょう。



マークアップ
<merge xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <ImageButton style="@style/ImageButton" android:id="@+id/newTabButton" android:layout_width="@dimen/toolbar_button_size" android:layout_height="@dimen/toolbar_button_size" android:layout_gravity="start" android:contentDescription="@string/content_desc_add_tab" android:src="@drawable/ic_plus" /> <Button android:id="@+id/tabSwitcher" android:layout_width="@dimen/toolbar_button_size" android:layout_height="@dimen/toolbar_button_size" android:layout_gravity="end" android:enabled="false" /> <com.bejibx.webviewexample.widget.UrlBar android:id="@+id/urlContainer" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_margin="5dp" android:freezesText="true" android:hint="@string/hint_url_container" android:imeOptions="actionGo|flagNoExtractUi|flagNoFullscreen" android:inputType="textUri" android:paddingLeft="8dp" android:paddingRight="8dp" android:singleLine="true" android:visibility="gone" /> </merge>
      
      







コード
 public class ToolbarLayout extends ViewGroup { private static final String TAG = ToolbarLayout.class.getSimpleName(); private static final boolean DEBUG = true; private ImageButton mNewTabButton; private Button mTabSwitchButton; private UrlBar mUrlContainer; public ToolbarLayout(Context context) { super(context); initializeViews(context); } public ToolbarLayout(Context context, AttributeSet attrs) { super(context, attrs); initializeViews(context); } public ToolbarLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initializeViews(context); } @TargetApi(Build.VERSION_CODES.LOLLIPOP) public ToolbarLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); initializeViews(context); } private void initializeViews(Context context) { LayoutInflater.from(context).inflate(R.layout.fragment_address_bar_template, this, true); mUrlContainer = (UrlBar) findViewById(R.id.urlContainer); mNewTabButton = (ImageButton) findViewById(R.id.newTabButton); mTabSwitchButton = (Button) findViewById(R.id.tabSwitcher); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (DEBUG) { Log.d(TAG, LogHelper.onMeasure(widthMeasureSpec, heightMeasureSpec)); } int widthConstrains = getPaddingLeft() + getPaddingRight(); final int heightConstrains = getPaddingTop() + getPaddingBottom(); int totalHeightUsed = heightConstrains; int childTotalWidth; int childTotalHeight; MarginLayoutParams lp; measureChildWithMargins( mNewTabButton, widthMeasureSpec, widthConstrains, heightMeasureSpec, heightConstrains); lp = (MarginLayoutParams) mNewTabButton.getLayoutParams(); childTotalWidth = mNewTabButton.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; childTotalHeight = mNewTabButton.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; widthConstrains += childTotalWidth; totalHeightUsed += childTotalHeight; measureChildWithMargins( mTabSwitchButton, widthMeasureSpec, widthConstrains, heightMeasureSpec, heightConstrains); lp = (MarginLayoutParams) mTabSwitchButton.getLayoutParams(); childTotalWidth = mTabSwitchButton.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; childTotalHeight = mTabSwitchButton.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; widthConstrains += childTotalWidth; totalHeightUsed = Math.max(childTotalHeight + heightConstrains, totalHeightUsed); /* * [FIXED] find out how to handle match_parent here * There was not a problem with match_parent interaction here. The real problem is * layout_height="wrap_content" on high-level container cause EditText to measure it's * height improperly. For now I'm just set layout_height on high-level layout to fixed value * (this make sense because of top-level layout structure, see activity_main.xml) which * measure EditText correctly. * * TODO I'm steel need to figure out whats going wrong in this particular case. */ if (mUrlContainer.getVisibility() != GONE) { measureChildWithMargins( mUrlContainer, widthMeasureSpec, widthConstrains, heightMeasureSpec, heightConstrains); lp = (MarginLayoutParams) mUrlContainer.getLayoutParams(); childTotalWidth = mUrlContainer.getMeasuredWidth() + lp.leftMargin + lp.rightMargin; childTotalHeight = mUrlContainer.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; widthConstrains += childTotalWidth; totalHeightUsed = Math.max(childTotalHeight + heightConstrains, totalHeightUsed); } final int totalWidthUsed = widthConstrains; setMeasuredDimension( resolveSize(totalWidthUsed, widthMeasureSpec), resolveSize(totalHeightUsed, heightMeasureSpec)); } @Override protected void onLayout(boolean changed, int parentLeft, int parentTop, int parentRight, int parentBottom) { if (DEBUG) { Log.d(TAG, LogHelper.onLayout(changed, parentLeft, parentTop, parentRight, parentBottom)); } /* * Layout order: * 1. Layout "New tab" button on the left side. * 2. Layout "Tab switch" button on the right side. * 3. If url container is unfocused, layout it between "New tab" and "Tab switch" buttons. * Otherwise layout it accordingly to mUrlContainerExpandedRect bounds. */ int paddingLeft = getPaddingLeft(); int paddingRight = getPaddingRight(); int paddingTop = getPaddingTop(); /* * Edges for url container left and right bounds. Move it during layout childs * located to right and left of url container. */ int leftEdge = parentLeft + paddingLeft; int rightEdge = parentRight - paddingRight; int childLeft, childTop, childRight, childBottom, childWidth, childHeight; if (mNewTabButton.getVisibility() != GONE) { MarginLayoutParams lp = (MarginLayoutParams) mNewTabButton.getLayoutParams(); childWidth = mNewTabButton.getMeasuredWidth(); childHeight = mNewTabButton.getMeasuredHeight(); childLeft = parentLeft + paddingLeft + lp.leftMargin; childTop = parentTop + paddingTop + lp.topMargin; childRight = childLeft + childWidth; childBottom = childTop + childHeight; mNewTabButton.layout(childLeft, childTop, childRight, childBottom); leftEdge = childRight + lp.rightMargin; } if (mTabSwitchButton.getVisibility() != GONE) { MarginLayoutParams lp = (MarginLayoutParams) mTabSwitchButton.getLayoutParams(); childWidth = mTabSwitchButton.getMeasuredWidth(); childHeight = mTabSwitchButton.getMeasuredHeight(); childRight = parentRight - paddingRight - lp.rightMargin; childTop = parentTop + paddingTop + lp.topMargin; childLeft = childRight - childWidth; childBottom = childTop + childHeight; mTabSwitchButton.layout(childLeft, childTop, childRight, childBottom); rightEdge = childLeft - lp.leftMargin; } if (mUrlContainer.getVisibility() != GONE) { MarginLayoutParams lp = (MarginLayoutParams) mUrlContainer.getLayoutParams(); childHeight = mUrlContainer.getMeasuredHeight(); childLeft = leftEdge + lp.leftMargin; childTop = parentTop + paddingTop + lp.topMargin; childRight = rightEdge - lp.rightMargin; childBottom = childTop + childHeight; mUrlContainer.layout(childLeft, childTop, childRight, childBottom); } } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new MarginLayoutParams(getContext(), attrs); } @Override protected LayoutParams generateLayoutParams(LayoutParams p) { return new MarginLayoutParams(p); } @Override protected LayoutParams generateDefaultLayoutParams() { return new MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); } @Override protected void measureChildWithMargins( @NonNull View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { MarginLayoutParams layoutParams = (MarginLayoutParams) child.getLayoutParams(); int childWidthMeasureSpec = getChildMeasureSpec( parentWidthMeasureSpec, widthUsed + layoutParams.leftMargin + layoutParams.rightMargin, layoutParams.width); int childHeightMeasureSpec = getChildMeasureSpec( parentHeightMeasureSpec, heightUsed + layoutParams.topMargin + layoutParams.bottomMargin, layoutParams.height); child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } }
      
      







マークアップには3つの要素が含まれます。



  1. [タブの追加]ボタンのサイズは固定されており、左側にあります。
  2. [タブの選択]ボタンのサイズは固定で、右側にあります。
  3. URLの入力フィールド(EditTextの後継であるUrlBar)が残りの空きスペースを埋めます。


onMeasureメソッドとonLayoutメソッドは複雑ではありません。最初にボタンを測定/配置し、次にそれらの間にテキストフィールドを配置します。



別の例の上でこれをすべて行ったので、冗長なコードの存在に気付くことができます。 たとえば、「タブの追加」ボタン。 タブ選択モードに切り替えたときにのみ表示されますが、この場合は単に非表示になっています。



アニメーターを追加



最初に、アニメーション中に変化するパラメーターを追加します。 AnimatorからUrlBarを直接サイズ変更するのではなく、現在のアニメーションの進行状況をパーセンテージで表示する変数を導入します。



 private static final float URL_FOCUS_CHANGE_FOCUSED_PERCENT = 1.0f; private static final float URL_FOCUS_CHANGE_UNFOCUSED_PERCENT = 0.0f; /** * 1.0 is 100% focused, 0 is unfocused */ private float mUrlFocusChangePercent;
      
      





ObjectAnimatorを使用するため、パラメーターにゲッターとセッターを追加する必要がありますが、minSdkVersion> = 14の場合、反射を避けるために、このためにPropertyクラスのフィールドを作成することをお勧めします。



 /** * Use actual property to avoid reflection when creating animators. For api from * 11 (3.0.X Honeycomb) to 13 (3.2 Honeycomb_mr2) we should use reflection (see {@link <a href="http://developer.android.com/guide/topics/graphics/prop-animation.html#object-animator">Animating with ObjectAnimator</a>}). * For older apis I'll recommend to use {@link <a href="http://nineoldandroids.com/">NineOldAndroids</a>} library. */ private final Property<ToolbarLayout, Float> mUrlFocusChangePercentProperty = new Property<ToolbarLayout, Float>(Float.class, "") { @Override public void set(ToolbarLayout object, Float value) { mUrlFocusChangePercent = value; mUrlContainer.invalidate(); invalidate(); } @Override public Float get(ToolbarLayout object) { return object.mUrlFocusChangePercent; } };
      
      





次に、2つの内部クラスと2つのフィールドを追加して、アニメーションを開始します。



 private boolean mDisableRelayout; private final UrlContainerFocusChangeListener mUrlContainerFocusChangeListener = new UrlContainerFocusChangeListener(); private class UrlContainerFocusChangeListener implements OnFocusChangeListener { @Override public void onFocusChange(View v, boolean hasFocus) { if (DEBUG) { Log.d(TAG, LogHelper.onFocusChange(hasFocus)); } // Trigger url focus animation if (mUrlFocusingLayoutAnimator != null && mUrlFocusingLayoutAnimator.isRunning()) { mUrlFocusingLayoutAnimator.cancel(); mUrlFocusingLayoutAnimator = null; } List<Animator> animators = new ArrayList<>(); Animator animator; if (hasFocus) { animator = ObjectAnimator.ofFloat(this, mUrlFocusChangePercentProperty, URL_FOCUS_CHANGE_FOCUSED_PERCENT); } else { animator = ObjectAnimator.ofFloat(this, mUrlFocusChangePercentProperty, URL_FOCUS_CHANGE_UNFOCUSED_PERCENT); } animator.setDuration(URL_FOCUS_CHANGE_ANIMATION_DURATION_MS); animator.setInterpolator(BakedBezierInterpolator.TRANSFORM_CURVE); animators.add(animator); mUrlFocusingLayoutAnimator = new AnimatorSet(); mUrlFocusingLayoutAnimator.playTogether(animators); mUrlFocusingLayoutAnimator.addListener(new UrlFocusingAnimatorListenerAdapter(hasFocus)); mUrlFocusingLayoutAnimator.start(); } } private class UrlFocusingAnimatorListenerAdapter extends AnimatorListenerAdapter { private final boolean mHasFocus; public UrlFocusingAnimatorListenerAdapter(boolean hasFocus) { super(); mHasFocus = hasFocus; } @Override public void onAnimationEnd(Animator animation) { mDisableRelayout = false; if (!hasFocus()) { mTabSwitchButton.setVisibility(VISIBLE); requestLayout(); } } @Override public void onAnimationStart(Animator animation) { if (mHasFocus) { mTabSwitchButton.setVisibility(GONE); requestLayout(); } else { mDisableRelayout = true; } } }
      
      





OnFocusChangeListenerをinitializeViewsに登録することを忘れないでください!

 private void initializeViews(Context context) { //... mUrlContainer.setOnFocusChangeListener(mUrlContainerFocusChangeListener); }
      
      





このステップでは、アニメーションメカニズム自体のロジックは終了し、視覚的なコンポーネントは残りますが、まず、何、なぜ、なぜかを見ていきます。



  1. フォーカスが変更されると、ObjectAnimatorを作成します。このオブジェクトは、変数を段階的に変更し、フィールドが受け取ったフォーカスの割合を示します。
  2. 各ステップで、ViewGroupに対してinvalidate()が呼び出されます。 この方法では、パーティションの再作成は行われず、コンポーネントが再描画されるだけです。


UrlBarでフォーカスを取得するプロセスは次のとおりです。



  1. 他のすべての要素を非表示にして、アニメーションのレンダリングを妨げないようにします(この場合、これはタブを切り替えるためのボタンです)。
  2. requestLayout()を呼び出して、アニメーションが完了した後、UrlBarの実際の境界が観察された境界と一致するようにします(requestLayout()を呼び出した後、onMeasure + onLayoutメソッドを遅延して呼び出すことができます!)。
  3. アニメーションの割合を段階的に変更し、各ステップでinvalidate()を呼び出します。
  4. 各ステップで手動で現在の割合のUrlBarの境界を計算し、再描画します。


UrlBarでフォーカスを失った場合、要素を非表示にし、反対に、アニメーションの最後にrequestLayout()を呼び出す必要があります。 また、マークアップフェーズを無効にする変数を導入し、onMeasureメソッドとonLayoutメソッドに変更を追加することを忘れないでください。



 private boolean mDisableRelayout; @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (!mDisableRelayout) { // ... } else { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } @Override protected void onLayout(boolean changed, int parentLeft, int parentTop, int parentRight, int parentBottom) { if (!mDisableRelayout) { // ... } }
      
      





描く準備をする



各ステップでUrlBarのサイズを計算するには、その初期サイズと最終サイズを知る必要があります。 このサイズを記憶する2つの変数を追加し、onLayoutを少し変更します。



 /** * Rectangle, which represents url container bounds relative to it's * parent bounds when unfocused. */ private final Rect mUrlContainerCollapsedRect = new Rect(); /** * Rectangle, which represents url container bounds relative to it's * parent bounds when FOCUSED. */ private final Rect mUrlContainerExpandedRect = new Rect(); @Override protected void onLayout(boolean changed, int parentLeft, int parentTop, int parentRight, int parentBottom) { //... updateUrlBarCollapsedRect(); /* *     UrlBar'.     UrlBar   ViewGroup. */ mUrlContainerExpandedRect.set(0, 0, parentRight, parentBottom); } /* *   UrlBar'  .       *   ,          . */ private void updateUrlBarCollapsedRect() { int paddingLeft = getPaddingLeft(); int paddingRight = getPaddingRight(); int paddingTop = getPaddingTop(); int rightEdge = getMeasuredWidth() - paddingRight; MarginLayoutParams lp = (MarginLayoutParams) mTabSwitchButton.getLayoutParams(); rightEdge -= (lp.leftMargin + mTabSwitchButton.getMeasuredWidth() + lp.rightMargin); lp = (MarginLayoutParams) mUrlContainer.getLayoutParams(); int childHeight = mUrlContainer.getMeasuredHeight(); int childLeft = paddingLeft + lp.leftMargin; int childTop = paddingTop + lp.topMargin; int childRight = rightEdge - lp.rightMargin; int childBottom = childTop + childHeight; mUrlContainerCollapsedRect.set(childLeft, childTop, childRight, childBottom); }
      
      





描く!



アニメーション中に直接、UrlBarの実際のサイズは変化せず、アニメーションの開始時または終了時に発生します。デフォルトでは、マークアップ段階で取得した境界に従って描画されます。 したがって、アニメーション中、コンポーネントの実際のサイズは、観測されたものよりも大きくなります。 この状況でUrlBarを描画するときに観察されるサイズを小さくするには、トリックを使用します-canvasでclipRectを実行します



もう1つの方法は、UrlBarから背景を削除して、手動で描画することです。



レイアウトを少し変更します。



 <com.bejibx.webviewexample.widget.UrlBar ... android:background="@null" />
      
      





背景を描画する変数を導入します。



 private Drawable mUrlContainerBackground; /** * Variable to store url background padding's. This is important when we use * 9-patch as background drawable. */ private final Rect mUrlBackgroundPadding = new Rect(); private void initializeViews(Context context) { //... mUrlContainerBackground = ApiCompatibilityHelper.getDrawable(getResources(), R.drawable.textbox); mUrlContainerBackground.getPadding(mUrlBackgroundPadding); }
      
      





そして最後に、レンダリング! UrlBarの条件をdrawChild (Canvas、View、long)メソッドに追加します。



 @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { if (child == mUrlContainer) { boolean clipped = false; if (mUrlContainerBackground != null) { canvas.save(); int clipLeft = mUrlContainerCollapsedRect.left; int clipTop = mUrlContainerCollapsedRect.top; int clipRight = mUrlContainerCollapsedRect.right; int clipBottom = mUrlContainerCollapsedRect.bottom; int expandedLeft = mUrlContainerExpandedRect.left - mUrlBackgroundPadding.left; int expandedTop = mUrlContainerExpandedRect.top - mUrlBackgroundPadding.top; int expandedRight = mUrlContainerExpandedRect.right + mUrlBackgroundPadding.right; int expandedBottom = mUrlContainerExpandedRect.bottom + mUrlBackgroundPadding.bottom; if (mUrlFocusChangePercent == URL_FOCUS_CHANGE_FOCUSED_PERCENT) { clipLeft = expandedLeft; clipTop = expandedTop; clipRight = expandedRight; clipBottom = expandedBottom; } else { // No need to compute those when url bar completely focused or unfocused. int deltaLeft = clipLeft - expandedLeft; int deltaTop = clipTop - expandedTop; int deltaRight = expandedRight - clipRight; int deltaBottom = expandedBottom - clipBottom; clipLeft -= deltaLeft * mUrlFocusChangePercent; clipTop -= deltaTop * mUrlFocusChangePercent; clipRight += deltaRight * mUrlFocusChangePercent; clipBottom += deltaBottom * mUrlFocusChangePercent; } mUrlContainerBackground.setBounds(clipLeft, clipTop, clipRight, clipBottom); mUrlContainerBackground.draw(canvas); canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom); clipped = true; } boolean result = super.drawChild(canvas, mUrlContainer, drawingTime); if (clipped) { canvas.restore(); } return result; } return super.drawChild(canvas, child, drawingTime); }
      
      





すべての準備が整いました。実行して見ることができます:







おわりに



仕事に取り掛かって、私はこの仕事が簡単で、ある夜に文字通りそれを処理できると思っていました。 もう一度このレーキに出会います。 現在の実装オプションやコメントが他にある場合は、コメントで共有してください。



この例が誰かに役立つことを心から願っています。 幸運を祈ります。流れるようなアニメーションがあなたと共に来ますように!



All Articles