Hi%username%! 今日は、Androidアプリケーションのコンポーネントのアニメートされたサイズ変更を
追加の労力なしで実装する方法を皆さんと共有したいと思います。
私はアニメーションについて多くのことを読みましたが、それでもインターフェースでそれを使用することができませんでした。 私は最終的に、あらゆる種類の
レイアウト遷移 、
アニメーター 、
レイアウトアニメーションを試し、このテーマに関する記事を書きたいと思いました。 ただし、カスタム
ViewGroupと
ObjectAnimatorという
はるかに散文的なものになりました。
そこで、次のように、Chrome for Androidのように、フォーカスを受け取ったときに
EditTextを展開したかったのです。
StackOverflowをすばやくスクロールすると、おおよその移動方向を決定するための2つの実装オプションが見つかりました。
- ScaleAnimationを使用します 。
- いずれにせよ、ステップごとに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); 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)); } int paddingLeft = getPaddingLeft(); int paddingRight = getPaddingRight(); int paddingTop = getPaddingTop(); 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つの要素が含まれます。
- [タブの追加]ボタンのサイズは固定されており、左側にあります。
- [タブの選択]ボタンのサイズは固定で、右側にあります。
- 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; private float mUrlFocusChangePercent;
ObjectAnimatorを使用するため、パラメーターにゲッターとセッターを追加する必要がありますが、minSdkVersion> = 14の場合、反射を避けるために、このために
Propertyクラスのフィールドを作成することをお勧めします。
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)); }
OnFocusChangeListenerをinitializeViewsに登録することを忘れないでください!
private void initializeViews(Context context) {
このステップでは、アニメーションメカニズム自体のロジックは終了し、視覚的なコンポーネントは残りますが、まず、何、なぜ、なぜかを見ていきます。
- フォーカスが変更されると、ObjectAnimatorを作成します。このオブジェクトは、変数を段階的に変更し、フィールドが受け取ったフォーカスの割合を示します。
- 各ステップで、ViewGroupに対してinvalidate()が呼び出されます。 この方法では、パーティションの再作成は行われず、コンポーネントが再描画されるだけです。
UrlBarでフォーカスを取得するプロセスは次のとおりです。
- 他のすべての要素を非表示にして、アニメーションのレンダリングを妨げないようにします(この場合、これはタブを切り替えるためのボタンです)。
- requestLayout()を呼び出して、アニメーションが完了した後、UrlBarの実際の境界が観察された境界と一致するようにします(requestLayout()を呼び出した後、onMeasure + onLayoutメソッドを遅延して呼び出すことができます!)。
- アニメーションの割合を段階的に変更し、各ステップでinvalidate()を呼び出します。
- 各ステップで手動で現在の割合のUrlBarの境界を計算し、再描画します。
UrlBarでフォーカスを失った場合、要素を非表示にし、反対に、アニメーションの最後にrequestLayout()を呼び出す必要があります。 また、マークアップフェーズを無効にする変数を導入し、onMeasureメソッドとonLayoutメソッドに変更を追加することを忘れないでください。
private boolean mDisableRelayout; @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (!mDisableRelayout) {
描く準備をする
各ステップでUrlBarのサイズを計算するには、その初期サイズと最終サイズを知る必要があります。 このサイズを記憶する2つの変数を追加し、onLayoutを少し変更します。
private final Rect mUrlContainerCollapsedRect = new Rect(); private final Rect mUrlContainerExpandedRect = new Rect(); @Override protected void onLayout(boolean changed, int parentLeft, int parentTop, int parentRight, int parentBottom) {
描く!
アニメーション中に直接、UrlBarの実際のサイズは変化せず、アニメーションの開始時または終了時に発生します。デフォルトでは、マークアップ段階で取得した境界に従って描画されます。 したがって、アニメーション中、コンポーネントの実際のサイズは、観測されたものよりも大きくなります。 この状況でUrlBarを描画するときに観察されるサイズを小さくするには、トリックを使用します
-canvasでclipRectを実行します 。
もう1つの方法は、UrlBarから背景を削除して、手動で描画することです。
レイアウトを少し変更します。
<com.bejibx.webviewexample.widget.UrlBar ... android:background="@null" />
背景を描画する変数を導入します。
private Drawable mUrlContainerBackground; private final Rect mUrlBackgroundPadding = new Rect(); private void initializeViews(Context context) {
そして最後に、レンダリング! 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 {
すべての準備が整いました。実行して見ることができます:
おわりに
仕事に取り掛かって、私はこの仕事が簡単で、ある夜に文字通りそれを処理できると思っていました。 もう一度このレーキに出会います。 現在の実装オプションやコメントが他にある場合は、コメントで共有してください。
この例が誰かに役立つことを心から願っています。 幸運を祈ります。流れるようなアニメーションがあなたと共に来ますように!