Merge "Add animation and positional stability to intent chooser UI" into mnc-dr-dev

This commit is contained in:
Adam Powell
2015-10-12 23:28:36 +00:00
committed by Android (Google) Code Review
5 changed files with 338 additions and 59 deletions

View File

@@ -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;
@@ -46,13 +49,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;
@@ -80,6 +88,7 @@ public class ChooserActivity extends ResolverActivity {
private Intent mReferrerFillInIntent;
private ChooserListAdapter mChooserListAdapter;
private ChooserRowAdapter mChooserRowAdapter;
private final List<ChooserTargetServiceConnection> mServiceConnections = new ArrayList<>();
@@ -253,7 +262,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);
}
@@ -913,19 +924,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();
}
@@ -933,19 +1028,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.
@@ -959,33 +1078,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);
@@ -994,34 +1147,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);
}
@@ -1048,6 +1193,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;
@@ -1199,4 +1362,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);
}
}
}

View File

@@ -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) {
@@ -1570,7 +1573,10 @@ public class ResolverActivity extends Activity {
private void onBindView(View view, TargetInfo info) {
final ViewHolder holder = (ViewHolder) view.getTag();
holder.text.setText(info.getDisplayLabel());
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());

View File

@@ -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,30 +753,43 @@ 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);
}
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;
}
updateCollapseOffset(oldCollapsibleHeight, !isDragging());
mTopOffset = Math.max(0, heightSize - heightUsed) + (int) mCollapseOffset;
setMeasuredDimension(sourceWidth, heightSize);
}
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;
}
}
if (lowest < heightUsed) {
heightUsed = lowest;
}
}
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
return lp.topMargin + heightUsed + lp.bottomMargin;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int width = getWidth();

View File

@@ -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"

View File

@@ -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>