Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6ef8578e authored by Adam Powell's avatar Adam Powell
Browse files

Add animation and positional stability to intent chooser UI

Dejank the process of bringing in new ChooserTargets from queried
services. Animate the service target rows in upward so that if the
user's finger is already headed for a visible choice we don't inject
something wrong right under them at the last second. Keep things sane
if the user is dragging the UI while we're bringing in new items.

To animate this, since we can't use RecyclerView from the framework we
treat the height of rows as a conceptual data set change for
ListView. To get away with doing this per-frame we pre-measure the
item height (which remains constant) instead of doing more expensive
wrap_content calculations. ResolverDrawerLayout is now aware of how to
account for a cheat-measured ListView to compensate.

Bug 24038066

Change-Id: I01414a5746815255ff948a6d0887bb5ad0897285
parent 86e153d5
Loading
Loading
Loading
Loading
+238 −35
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package com.android.internal.app;

import android.animation.ObjectAnimator;
import android.annotation.NonNull;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
@@ -29,6 +31,7 @@ import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.database.DataSetObserver;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.os.Bundle;
@@ -45,13 +48,18 @@ import android.service.chooser.ChooserTargetService;
import android.service.chooser.IChooserTargetResult;
import android.service.chooser.IChooserTargetService;
import android.text.TextUtils;
import android.util.FloatProperty;
import android.util.Log;
import android.util.Slog;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.MeasureSpec;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import android.widget.ListView;
@@ -79,6 +87,7 @@ public class ChooserActivity extends ResolverActivity {
    private Intent mReferrerFillInIntent;

    private ChooserListAdapter mChooserListAdapter;
    private ChooserRowAdapter mChooserRowAdapter;

    private final List<ChooserTargetServiceConnection> mServiceConnections = new ArrayList<>();

@@ -252,7 +261,9 @@ public class ChooserActivity extends ResolverActivity {
            boolean alwaysUseOption) {
        final ListView listView = adapterView instanceof ListView ? (ListView) adapterView : null;
        mChooserListAdapter = (ChooserListAdapter) adapter;
        adapterView.setAdapter(new ChooserRowAdapter(mChooserListAdapter));
        mChooserRowAdapter = new ChooserRowAdapter(mChooserListAdapter);
        mChooserRowAdapter.registerDataSetObserver(new OffsetDataSetObserver(adapterView));
        adapterView.setAdapter(mChooserRowAdapter);
        if (listView != null) {
            listView.setItemsCanFocus(true);
        }
@@ -899,19 +910,103 @@ public class ChooserActivity extends ResolverActivity {
        }
    }

    static class RowScale {
        private static final int DURATION = 400;

        float mScale;
        ChooserRowAdapter mAdapter;
        private final ObjectAnimator mAnimator;

        public static final FloatProperty<RowScale> PROPERTY =
                new FloatProperty<RowScale>("scale") {
            @Override
            public void setValue(RowScale object, float value) {
                object.mScale = value;
                object.mAdapter.notifyDataSetChanged();
            }

            @Override
            public Float get(RowScale object) {
                return object.mScale;
            }
        };

        public RowScale(@NonNull ChooserRowAdapter adapter, float from, float to) {
            mAdapter = adapter;
            mScale = from;
            if (from == to) {
                mAnimator = null;
                return;
            }

            mAnimator = ObjectAnimator.ofFloat(this, PROPERTY, from, to).setDuration(DURATION);
        }

        public RowScale setInterpolator(Interpolator interpolator) {
            if (mAnimator != null) {
                mAnimator.setInterpolator(interpolator);
            }
            return this;
        }

        public float get() {
            return mScale;
        }

        public void startAnimation() {
            if (mAnimator != null) {
                mAnimator.start();
            }
        }

        public void cancelAnimation() {
            if (mAnimator != null) {
                mAnimator.cancel();
            }
        }
    }

    class ChooserRowAdapter extends BaseAdapter {
        private ChooserListAdapter mChooserListAdapter;
        private final LayoutInflater mLayoutInflater;
        private final int mColumnCount = 4;
        private RowScale[] mServiceTargetScale;
        private final Interpolator mInterpolator;

        public ChooserRowAdapter(ChooserListAdapter wrappedAdapter) {
            mChooserListAdapter = wrappedAdapter;
            mLayoutInflater = LayoutInflater.from(ChooserActivity.this);

            mInterpolator = AnimationUtils.loadInterpolator(ChooserActivity.this,
                    android.R.interpolator.decelerate_quint);

            wrappedAdapter.registerDataSetObserver(new DataSetObserver() {
                @Override
                public void onChanged() {
                    super.onChanged();
                    final int rcount = getServiceTargetRowCount();
                    if (mServiceTargetScale == null
                            || mServiceTargetScale.length != rcount) {
                        RowScale[] old = mServiceTargetScale;
                        int oldRCount = old != null ? old.length : 0;
                        mServiceTargetScale = new RowScale[rcount];
                        if (old != null && rcount > 0) {
                            System.arraycopy(old, 0, mServiceTargetScale, 0,
                                    Math.min(old.length, rcount));
                        }

                        for (int i = rcount; i < oldRCount; i++) {
                            old[i].cancelAnimation();
                        }

                        for (int i = oldRCount; i < rcount; i++) {
                            final RowScale rs = new RowScale(ChooserRowAdapter.this, 0.f, 1.f)
                                    .setInterpolator(mInterpolator);
                            mServiceTargetScale[i] = rs;
                            rs.startAnimation();
                        }
                    }

                    notifyDataSetChanged();
                }

@@ -919,19 +1014,43 @@ public class ChooserActivity extends ResolverActivity {
                public void onInvalidated() {
                    super.onInvalidated();
                    notifyDataSetInvalidated();
                    if (mServiceTargetScale != null) {
                        for (RowScale rs : mServiceTargetScale) {
                            rs.cancelAnimation();
                        }
                    }
                }
            });
        }

        private float getRowScale(int rowPosition) {
            final int start = getCallerTargetRowCount();
            final int end = start + getServiceTargetRowCount();
            if (rowPosition >= start && rowPosition < end) {
                return mServiceTargetScale[rowPosition - start].get();
            }
            return 1.f;
        }

        @Override
        public int getCount() {
            return (int) (
                    Math.ceil((float) mChooserListAdapter.getCallerTargetCount() / mColumnCount)
                    + Math.ceil((float) mChooserListAdapter.getServiceTargetCount() / mColumnCount)
                    getCallerTargetRowCount()
                    + getServiceTargetRowCount()
                    + Math.ceil((float) mChooserListAdapter.getStandardTargetCount() / mColumnCount)
            );
        }

        public int getCallerTargetRowCount() {
            return (int) Math.ceil(
                    (float) mChooserListAdapter.getCallerTargetCount() / mColumnCount);
        }

        public int getServiceTargetRowCount() {
            return (int) Math.ceil(
                    (float) mChooserListAdapter.getServiceTargetCount() / mColumnCount);
        }

        @Override
        public Object getItem(int position) {
            // We have nothing useful to return here.
@@ -945,33 +1064,67 @@ public class ChooserActivity extends ResolverActivity {

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            final View[] holder;
            final RowViewHolder holder;
            if (convertView == null) {
                holder = createViewHolder(parent);
            } else {
                holder = (View[]) convertView.getTag();
                holder = (RowViewHolder) convertView.getTag();
            }
            bindViewHolder(position, holder);

            // We keep the actual list item view as the last item in the holder array
            return holder[mColumnCount];
            return holder.row;
        }

        View[] createViewHolder(ViewGroup parent) {
            final View[] holder = new View[mColumnCount + 1];

        RowViewHolder createViewHolder(ViewGroup parent) {
            final ViewGroup row = (ViewGroup) mLayoutInflater.inflate(R.layout.chooser_row,
                    parent, false);
            final RowViewHolder holder = new RowViewHolder(row, mColumnCount);
            final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);

            for (int i = 0; i < mColumnCount; i++) {
                holder[i] = mChooserListAdapter.createView(row);
                row.addView(holder[i]);
                final View v = mChooserListAdapter.createView(row);
                v.setOnClickListener(new OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        startSelected(holder.itemIndex, false, true);
                    }
                });
                v.setOnLongClickListener(new OnLongClickListener() {
                    @Override
                    public boolean onLongClick(View v) {
                        showAppDetails(
                                mChooserListAdapter.resolveInfoForPosition(holder.itemIndex, true));
                        return true;
                    }
                });
                row.addView(v);
                holder.cells[i] = v;

                // Force height to be a given so we don't have visual disruption during scaling.
                LayoutParams lp = v.getLayoutParams();
                v.measure(spec, spec);
                if (lp == null) {
                    lp = new LayoutParams(LayoutParams.MATCH_PARENT, v.getMeasuredHeight());
                    row.setLayoutParams(lp);
                } else {
                    lp.height = v.getMeasuredHeight();
                }
            }

            // Pre-measure so we can scale later.
            holder.measure();
            LayoutParams lp = row.getLayoutParams();
            if (lp == null) {
                lp = new LayoutParams(LayoutParams.MATCH_PARENT, holder.measuredRowHeight);
                row.setLayoutParams(lp);
            } else {
                lp.height = holder.measuredRowHeight;
            }
            row.setTag(holder);
            holder[mColumnCount] = row;
            return holder;
        }

        void bindViewHolder(int rowPosition, View[] holder) {
        void bindViewHolder(int rowPosition, RowViewHolder holder) {
            final int start = getFirstRowPosition(rowPosition);
            final int startType = mChooserListAdapter.getPositionTargetType(start);

@@ -980,34 +1133,26 @@ public class ChooserActivity extends ResolverActivity {
                end--;
            }

            final ViewGroup row = (ViewGroup) holder[mColumnCount];

            if (startType == ChooserListAdapter.TARGET_SERVICE) {
                row.setBackgroundColor(getColor(R.color.chooser_service_row_background_color));
                holder.row.setBackgroundColor(
                        getColor(R.color.chooser_service_row_background_color));
            } else {
                row.setBackground(null);
                holder.row.setBackgroundColor(Color.TRANSPARENT);
            }

            final int oldHeight = holder.row.getLayoutParams().height;
            holder.row.getLayoutParams().height = Math.max(1,
                    (int) (holder.measuredRowHeight * getRowScale(rowPosition)));
            if (holder.row.getLayoutParams().height != oldHeight) {
                holder.row.requestLayout();
            }

            for (int i = 0; i < mColumnCount; i++) {
                final View v = holder[i];
                final View v = holder.cells[i];
                if (start + i <= end) {
                    v.setVisibility(View.VISIBLE);
                    final int itemIndex = start + i;
                    mChooserListAdapter.bindView(itemIndex, v);
                    v.setOnClickListener(new OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            startSelected(itemIndex, false, true);
                        }
                    });
                    v.setOnLongClickListener(new OnLongClickListener() {
                        @Override
                        public boolean onLongClick(View v) {
                            showAppDetails(
                                    mChooserListAdapter.resolveInfoForPosition(itemIndex, true));
                            return true;
                        }
                    });
                    holder.itemIndex = start + i;
                    mChooserListAdapter.bindView(holder.itemIndex, v);
                } else {
                    v.setVisibility(View.GONE);
                }
@@ -1034,6 +1179,24 @@ public class ChooserActivity extends ResolverActivity {
        }
    }

    static class RowViewHolder {
        final View[] cells;
        final ViewGroup row;
        int measuredRowHeight;
        int itemIndex;

        public RowViewHolder(ViewGroup row, int cellCount) {
            this.row = row;
            this.cells = new View[cellCount];
        }

        public void measure() {
            final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
            row.measure(spec, spec);
            measuredRowHeight = row.getMeasuredHeight();
        }
    }

    static class ChooserTargetServiceConnection implements ServiceConnection {
        private final DisplayResolveInfo mOriginalTarget;
        private ComponentName mConnectedComponent;
@@ -1185,4 +1348,44 @@ public class ChooserActivity extends ResolverActivity {
            mSelectedTarget = null;
        }
    }

    class OffsetDataSetObserver extends DataSetObserver {
        private final AbsListView mListView;
        private int mCachedViewType = -1;
        private View mCachedView;

        public OffsetDataSetObserver(AbsListView listView) {
            mListView = listView;
        }

        @Override
        public void onChanged() {
            if (mResolverDrawerLayout == null) {
                return;
            }

            final int chooserTargetRows = mChooserRowAdapter.getServiceTargetRowCount();
            int offset = 0;
            for (int i = 0; i < chooserTargetRows; i++)  {
                final int pos = mChooserRowAdapter.getCallerTargetRowCount() + i;
                final int vt = mChooserRowAdapter.getItemViewType(pos);
                if (vt != mCachedViewType) {
                    mCachedView = null;
                }
                final View v = mChooserRowAdapter.getView(pos, mCachedView, mListView);
                int height = ((RowViewHolder) (v.getTag())).measuredRowHeight;

                offset += (int) (height * mChooserRowAdapter.getRowScale(pos) * chooserTargetRows);

                if (vt >= 0) {
                    mCachedViewType = vt;
                    mCachedView = v;
                } else {
                    mCachedViewType = -1;
                }
            }

            mResolverDrawerLayout.setCollapsibleHeightReserved(offset);
        }
    }
}
+7 −1
Original line number Diff line number Diff line
@@ -103,6 +103,8 @@ public class ResolverActivity extends Activity {
    private ResolverComparator mResolverComparator;
    private PickTargetOptionRequest mPickOptionRequest;

    protected ResolverDrawerLayout mResolverDrawerLayout;

    private boolean mRegistered;
    private final PackageMonitor mPackageMonitor = new PackageMonitor() {
        @Override public void onSomePackagesChanged() {
@@ -253,6 +255,7 @@ public class ResolverActivity extends Activity {
            if (isVoiceInteraction()) {
                rdl.setCollapsed(false);
            }
            mResolverDrawerLayout = rdl;
        }

        if (title == null) {
@@ -1567,7 +1570,10 @@ public class ResolverActivity extends Activity {

        private void onBindView(View view, TargetInfo info) {
            final ViewHolder holder = (ViewHolder) view.getTag();
            final CharSequence label = info.getDisplayLabel();
            if (!TextUtils.equals(holder.text.getText(), label)) {
                holder.text.setText(info.getDisplayLabel());
            }
            if (showsExtendedInfo(info)) {
                holder.text2.setVisibility(View.VISIBLE);
                holder.text2.setText(info.getExtendedInfo());
+90 −21
Original line number Diff line number Diff line
@@ -69,6 +69,12 @@ public class ResolverDrawerLayout extends ViewGroup {
    private int mCollapsibleHeight;
    private int mUncollapsibleHeight;

    /**
     * The height in pixels of reserved space added to the top of the collapsed UI;
     * e.g. chooser targets
     */
    private int mCollapsibleHeightReserved;

    private int mTopOffset;

    private boolean mIsDragging;
@@ -153,12 +159,62 @@ public class ResolverDrawerLayout extends ViewGroup {
        }
    }

    public void setCollapsibleHeightReserved(int heightPixels) {
        final int oldReserved = mCollapsibleHeightReserved;
        mCollapsibleHeightReserved = heightPixels;

        final int dReserved = mCollapsibleHeightReserved - oldReserved;
        if (dReserved != 0 && mIsDragging) {
            mLastTouchY -= dReserved;
        }

        final int oldCollapsibleHeight = mCollapsibleHeight;
        mCollapsibleHeight = Math.max(mCollapsibleHeight, getMaxCollapsedHeight());

        if (updateCollapseOffset(oldCollapsibleHeight, !isDragging())) {
            return;
        }

        invalidate();
    }

    private boolean isMoving() {
        return mIsDragging || !mScroller.isFinished();
    }

    private boolean isDragging() {
        return mIsDragging || getNestedScrollAxes() == SCROLL_AXIS_VERTICAL;
    }

    private boolean updateCollapseOffset(int oldCollapsibleHeight, boolean remainClosed) {
        if (oldCollapsibleHeight == mCollapsibleHeight) {
            return false;
        }

        if (isLaidOut()) {
            final boolean isCollapsedOld = mCollapseOffset != 0;
            if (remainClosed && (oldCollapsibleHeight < mCollapsibleHeight
                    && mCollapseOffset == oldCollapsibleHeight)) {
                // Stay closed even at the new height.
                mCollapseOffset = mCollapsibleHeight;
            } else {
                mCollapseOffset = Math.min(mCollapseOffset, mCollapsibleHeight);
            }
            final boolean isCollapsedNew = mCollapseOffset != 0;
            if (isCollapsedOld != isCollapsedNew) {
                notifyViewAccessibilityStateChangedIfNeeded(
                        AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
            }
        } else {
            // Start out collapsed at first unless we restored state for otherwise
            mCollapseOffset = mOpenOnLayout ? 0 : mCollapsibleHeight;
        }
        return true;
    }

    private int getMaxCollapsedHeight() {
        return isSmallCollapsed() ? mMaxCollapsedHeightSmall : mMaxCollapsedHeight;
        return (isSmallCollapsed() ? mMaxCollapsedHeightSmall : mMaxCollapsedHeight)
                + mCollapsibleHeightReserved;
    }

    public void setOnDismissedListener(OnDismissedListener listener) {
@@ -676,7 +732,7 @@ public class ResolverDrawerLayout extends ViewGroup {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (lp.alwaysShow && child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthSpec, widthPadding, heightSpec, heightUsed);
                heightUsed += lp.topMargin + child.getMeasuredHeight() + lp.bottomMargin;
                heightUsed += getHeightUsed(child);
            }
        }

@@ -688,7 +744,7 @@ public class ResolverDrawerLayout extends ViewGroup {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            if (!lp.alwaysShow && child.getVisibility() != GONE) {
                measureChildWithMargins(child, widthSpec, widthPadding, heightSpec, heightUsed);
                heightUsed += lp.topMargin + child.getMeasuredHeight() + lp.bottomMargin;
                heightUsed += getHeightUsed(child);
            }
        }

@@ -697,28 +753,41 @@ public class ResolverDrawerLayout extends ViewGroup {
                heightUsed - alwaysShowHeight - getMaxCollapsedHeight());
        mUncollapsibleHeight = heightUsed - mCollapsibleHeight;

        if (isLaidOut()) {
            final boolean isCollapsedOld = mCollapseOffset != 0;
            if (oldCollapsibleHeight < mCollapsibleHeight
                    && mCollapseOffset == oldCollapsibleHeight) {
                // Stay closed even at the new height.
                mCollapseOffset = mCollapsibleHeight;
            } else {
                mCollapseOffset = Math.min(mCollapseOffset, mCollapsibleHeight);
        updateCollapseOffset(oldCollapsibleHeight, !isDragging());

        mTopOffset = Math.max(0, heightSize - heightUsed) + (int) mCollapseOffset;

        setMeasuredDimension(sourceWidth, heightSize);
    }
            final boolean isCollapsedNew = mCollapseOffset != 0;
            if (isCollapsedOld != isCollapsedNew) {
                notifyViewAccessibilityStateChangedIfNeeded(
                        AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);

    private int getHeightUsed(View child) {
        // This method exists because we're taking a fast path at measuring ListViews that
        // lets us get away with not doing the more expensive wrap_content measurement which
        // imposes double child view measurement costs. If we're looking at a ListView, we can
        // check against the lowest child view plus padding and margin instead of the actual
        // measured height of the ListView. This lets the ListView hang off the edge when
        // all of the content would fit on-screen.

        int heightUsed = child.getMeasuredHeight();
        if (child instanceof AbsListView) {
            final AbsListView lv = (AbsListView) child;
            final int lvPaddingBottom = lv.getPaddingBottom();

            int lowest = 0;
            for (int i = 0, N = lv.getChildCount(); i < N; i++) {
                final int bottom = lv.getChildAt(i).getBottom() + lvPaddingBottom;
                if (bottom > lowest) {
                    lowest = bottom;
                }
        } else {
            // Start out collapsed at first unless we restored state for otherwise
            mCollapseOffset = mOpenOnLayout ? 0 : mCollapsibleHeight;
            }

        mTopOffset = Math.max(0, heightSize - heightUsed) + (int) mCollapseOffset;
            if (lowest < heightUsed) {
                heightUsed = lowest;
            }
        }

        setMeasuredDimension(sourceWidth, heightSize);
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        return lp.topMargin + heightUsed + lp.bottomMargin;
    }

    @Override
+1 −1
Original line number Diff line number Diff line
@@ -85,7 +85,7 @@

    <ListView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_height="match_parent"
            android:id="@+id/resolver_list"
            android:clipToPadding="false"
            android:scrollbarStyle="outsideOverlay"
+2 −1
Original line number Diff line number Diff line
@@ -70,6 +70,7 @@
              android:minLines="2"
              android:maxLines="2"
              android:gravity="top|center_horizontal"
              android:ellipsize="marquee" />
              android:ellipsize="marquee"
              android:visibility="gone" />
</LinearLayout>