From d9ddb3ba5bd4f3377c256bdd61dc08121a412659 Mon Sep 17 00:00:00 2001 From: "Garfield, Tan" Date: Fri, 27 May 2016 15:02:35 -0700 Subject: [PATCH] Suppress auto-closing drawer and add ripple effect on spring load roots. Bug: 28865182 Change-Id: Ief7967e33b9a0d7e94a667172121d8007f78115b (cherry picked from commit 17182ca46f7a8f3ae03bb8f5a16116246a5fbd91) --- .../res/drawable/root_item_background.xml | 35 ++++++ .../DocumentsUI/res/layout/fragment_roots.xml | 1 + packages/DocumentsUI/res/layout/item_root.xml | 8 +- packages/DocumentsUI/res/values/attrs.xml | 4 + packages/DocumentsUI/res/values/tags.xml | 1 + .../com/android/documentsui/BaseActivity.java | 2 - .../android/documentsui/ItemDragListener.java | 9 ++ .../com/android/documentsui/RootItemView.java | 55 +++++++++ .../android/documentsui/RootsFragment.java | 113 ++++++++++++++---- .../documentsui/ItemDragListenerTest.java | 25 +++- .../documentsui/testing/DragEvents.java | 8 ++ .../documentsui/testing/TestDrawable.java | 53 ++++++++ .../android/documentsui/testing/Views.java | 5 + 13 files changed, 290 insertions(+), 29 deletions(-) create mode 100644 packages/DocumentsUI/res/drawable/root_item_background.xml create mode 100644 packages/DocumentsUI/src/com/android/documentsui/RootItemView.java create mode 100644 packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestDrawable.java diff --git a/packages/DocumentsUI/res/drawable/root_item_background.xml b/packages/DocumentsUI/res/drawable/root_item_background.xml new file mode 100644 index 0000000000000..cc56f1e56e4d1 --- /dev/null +++ b/packages/DocumentsUI/res/drawable/root_item_background.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/DocumentsUI/res/layout/fragment_roots.xml b/packages/DocumentsUI/res/layout/fragment_roots.xml index b33b8d09b992d..ae462072c6b35 100644 --- a/packages/DocumentsUI/res/layout/fragment_roots.xml +++ b/packages/DocumentsUI/res/layout/fragment_roots.xml @@ -19,5 +19,6 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:paddingTop="8dp" + android:listSelector="@android:color/transparent" android:drawSelectorOnTop="true" android:divider="@null" /> diff --git a/packages/DocumentsUI/res/layout/item_root.xml b/packages/DocumentsUI/res/layout/item_root.xml index 816cb8a0d556e..3e447c9ee33df 100644 --- a/packages/DocumentsUI/res/layout/item_root.xml +++ b/packages/DocumentsUI/res/layout/item_root.xml @@ -14,7 +14,8 @@ limitations under the License. --> - + android:baselineAligned="false" + android:background="@drawable/root_item_background"> - + diff --git a/packages/DocumentsUI/res/values/attrs.xml b/packages/DocumentsUI/res/values/attrs.xml index 9e130011496b1..b48c52f4c4ffe 100644 --- a/packages/DocumentsUI/res/values/attrs.xml +++ b/packages/DocumentsUI/res/values/attrs.xml @@ -17,4 +17,8 @@ + + + + diff --git a/packages/DocumentsUI/res/values/tags.xml b/packages/DocumentsUI/res/values/tags.xml index 1c4b0ca8adffa..a7ff3d6c81ccf 100644 --- a/packages/DocumentsUI/res/values/tags.xml +++ b/packages/DocumentsUI/res/values/tags.xml @@ -17,4 +17,5 @@ + \ No newline at end of file diff --git a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java index 3597a7495b0b4..d1285c83cc89d 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java +++ b/packages/DocumentsUI/src/com/android/documentsui/BaseActivity.java @@ -256,8 +256,6 @@ public abstract class BaseActivity extends Activity } else { new PickRootTask(this, root).executeOnExecutor(getExecutorForCurrentDirectory()); } - - mNavigator.revealRootsDrawer(false); } @Override diff --git a/packages/DocumentsUI/src/com/android/documentsui/ItemDragListener.java b/packages/DocumentsUI/src/com/android/documentsui/ItemDragListener.java index 2c018f882bd28..66b94fc64da95 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/ItemDragListener.java +++ b/packages/DocumentsUI/src/com/android/documentsui/ItemDragListener.java @@ -17,6 +17,7 @@ package com.android.documentsui; import android.content.ClipData; +import android.graphics.drawable.Drawable; import android.util.Log; import android.view.DragEvent; import android.view.View; @@ -62,6 +63,7 @@ public class ItemDragListener implements OnDragListener { handleEnteredEvent(v); return true; case DragEvent.ACTION_DRAG_LOCATION: + handleLocationEvent(v, event.getX(), event.getY()); return true; case DragEvent.ACTION_DRAG_EXITED: case DragEvent.ACTION_DRAG_ENDED: @@ -83,6 +85,13 @@ public class ItemDragListener implements OnDragListener { mHoverTimer.schedule(task, ViewConfiguration.getLongPressTimeout()); } + private void handleLocationEvent(View v, float x, float y) { + Drawable background = v.getBackground(); + if (background != null) { + background.setHotspot(x, y); + } + } + private void handleExitedEndedEvent(View v) { mDragHost.setDropTargetHighlight(v, false); diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootItemView.java b/packages/DocumentsUI/src/com/android/documentsui/RootItemView.java new file mode 100644 index 0000000000000..93aa526aaa823 --- /dev/null +++ b/packages/DocumentsUI/src/com/android/documentsui/RootItemView.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.documentsui; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.LinearLayout; + +public final class RootItemView extends LinearLayout { + private static final int[] STATE_HIGHLIGHTED = {R.attr.state_highlighted}; + + private boolean mHighlighted = false; + + public RootItemView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + public int[] onCreateDrawableState(int extraSpace) { + final int[] drawableState = super.onCreateDrawableState(extraSpace + 1); + + if (mHighlighted) { + mergeDrawableStates(drawableState, STATE_HIGHLIGHTED); + } + + return drawableState; + } + + public void setHighlight(boolean highlight) { + mHighlighted = highlight; + refreshDrawableState(); + } + + /** + * Synthesizes pressed state to trick RippleDrawable starting a ripple effect. + */ + public void drawRipple() { + setPressed(true); + setPressed(false); + } +} diff --git a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java index ad2ee07c85a98..b333379af0048 100644 --- a/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java +++ b/packages/DocumentsUI/src/com/android/documentsui/RootsFragment.java @@ -18,6 +18,7 @@ package com.android.documentsui; import static com.android.documentsui.Shared.DEBUG; +import android.annotation.LayoutRes; import android.app.Activity; import android.app.Fragment; import android.app.FragmentManager; @@ -26,12 +27,13 @@ import android.app.LoaderManager.LoaderCallbacks; import android.content.Context; import android.content.Intent; import android.content.Loader; +import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.Bundle; +import android.os.Looper; import android.provider.Settings; -import android.support.annotation.ColorRes; import android.support.annotation.Nullable; import android.text.TextUtils; import android.text.format.Formatter; @@ -54,7 +56,9 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; /** @@ -198,6 +202,10 @@ public class RootsFragment extends Fragment implements ItemDragListener.DragHost */ @Override public void onViewHovered(View view) { + // SpacerView doesn't have DragListener so this view is guaranteed to be a RootItemView. + RootItemView itemView = (RootItemView) view; + itemView.drawRipple(); + final int position = (Integer) view.getTag(R.id.item_position_tag); final Item item = mAdapter.getItem(position); item.open(this); @@ -205,10 +213,9 @@ public class RootsFragment extends Fragment implements ItemDragListener.DragHost @Override public void setDropTargetHighlight(View v, boolean highlight) { - @ColorRes int colorId = highlight ? R.color.item_doc_background_selected - : android.R.color.transparent; - - v.setBackgroundColor(getActivity().getColor(colorId)); + // SpacerView doesn't have DragListener so this view is guaranteed to be a RootItemView. + RootItemView itemView = (RootItemView) v; + itemView.setHighlight(highlight); } private OnItemClickListener mItemListener = new OnItemClickListener() { @@ -216,6 +223,8 @@ public class RootsFragment extends Fragment implements ItemDragListener.DragHost public void onItemClick(AdapterView parent, View view, int position, long id) { final Item item = mAdapter.getItem(position); item.open(RootsFragment.this); + + ((BaseActivity) getActivity()).setRootsDrawerOpen(false); } }; @@ -223,32 +232,34 @@ public class RootsFragment extends Fragment implements ItemDragListener.DragHost @Override public boolean onItemLongClick(AdapterView parent, View view, int position, long id) { final Item item = mAdapter.getItem(position); - if (item instanceof AppItem) { - showAppDetails(((AppItem) item).info); - return true; - } else { - return false; - } + return item.showAppDetails(RootsFragment.this); } }; private static abstract class Item { - private final int mLayoutId; + private final @LayoutRes int mLayoutId; + private final String mStringId; - public Item(int layoutId) { + public Item(@LayoutRes int layoutId, String stringId) { mLayoutId = layoutId; + mStringId = stringId; } public View getView(View convertView, ViewGroup parent) { - // Disable recycling views because 1) it's very unlikely a view can be recycled here; - // 2) there is no easy way for us to know with which layout id the convertView was - // inflated; and 3) simplicity is much appreciated at this time. - convertView = LayoutInflater.from(parent.getContext()) + if (convertView == null + || (Integer) convertView.getTag(R.id.layout_id_tag) != mLayoutId) { + convertView = LayoutInflater.from(parent.getContext()) .inflate(mLayoutId, parent, false); + } + convertView.setTag(R.id.layout_id_tag, mLayoutId); bindView(convertView); return convertView; } + boolean showAppDetails(RootsFragment fragment) { + return false; + } + abstract void bindView(View convertView); abstract boolean isDropTarget(); @@ -257,13 +268,23 @@ public class RootsFragment extends Fragment implements ItemDragListener.DragHost } private static class RootItem extends Item { + private static final String STRING_ID_FORMAT = "RootItem{%s/%s}"; + public final RootInfo root; public RootItem(RootInfo root) { - super(R.layout.item_root); + super(R.layout.item_root, getStringId(root)); this.root = root; } + private static String getStringId(RootInfo root) { + // Empty URI authority is invalid, so we can use empty string if root.authority is null. + // Directly passing null to String.format() will write "null" which can be a valid URI + // authority. + String authority = (root.authority == null ? "" : root.authority); + return String.format(STRING_ID_FORMAT, authority, root.rootId); + } + @Override public void bindView(View convertView) { final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon); @@ -291,7 +312,7 @@ public class RootsFragment extends Fragment implements ItemDragListener.DragHost } @Override - public void open(RootsFragment fragment) { + void open(RootsFragment fragment) { BaseActivity activity = BaseActivity.get(fragment); Metrics.logRootVisited(fragment.getActivity(), root); activity.onRootPicked(root); @@ -299,8 +320,11 @@ public class RootsFragment extends Fragment implements ItemDragListener.DragHost } private static class SpacerItem extends Item { + private static final String STRING_ID = "SpacerItem"; + public SpacerItem() { - super(R.layout.item_root_spacer); + // Multiple spacer items can share the same string id as they're identical. + super(R.layout.item_root_spacer, STRING_ID); } @Override @@ -314,19 +338,35 @@ public class RootsFragment extends Fragment implements ItemDragListener.DragHost } @Override - public void open(RootsFragment fragment) { + void open(RootsFragment fragment) { if (DEBUG) Log.d(TAG, "Ignoring click/hover on spacer item."); } } private static class AppItem extends Item { + private static final String STRING_ID_FORMAT = "AppItem{%s/%s}"; + public final ResolveInfo info; public AppItem(ResolveInfo info) { - super(R.layout.item_root); + super(R.layout.item_root, getStringId(info)); this.info = info; } + private static String getStringId(ResolveInfo info) { + ActivityInfo activityInfo = info.activityInfo; + + String component = String.format( + STRING_ID_FORMAT, activityInfo.applicationInfo.packageName, activityInfo.name); + return component; + } + + @Override + boolean showAppDetails(RootsFragment fragment) { + fragment.showAppDetails(info); + return true; + } + @Override void bindView(View convertView) { final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon); @@ -348,7 +388,7 @@ public class RootsFragment extends Fragment implements ItemDragListener.DragHost } @Override - public void open(RootsFragment fragment) { + void open(RootsFragment fragment) { DocumentsActivity activity = DocumentsActivity.get(fragment); Metrics.logAppVisited(fragment.getActivity(), info); activity.onAppPicked(info); @@ -356,6 +396,9 @@ public class RootsFragment extends Fragment implements ItemDragListener.DragHost } private static class RootsAdapter extends ArrayAdapter { + private static final Map sIdMap = new HashMap(); + // the next available id to associate with a new string id + private static long sNextAvailableId; private OnDragListener mDragListener; @@ -429,6 +472,30 @@ public class RootsFragment extends Fragment implements ItemDragListener.DragHost } } + @Override + public boolean hasStableIds() { + return true; + } + + @Override + public long getItemId(int position) { + // Ensure this method is only called in main thread because we don't have any + // concurrency protection. + assert(Looper.myLooper() == Looper.getMainLooper()); + + String stringId = getItem(position).mStringId; + + long id; + if (sIdMap.containsKey(stringId)) { + id = sIdMap.get(stringId); + } else { + id = sNextAvailableId++; + sIdMap.put(stringId, id); + } + + return id; + } + @Override public View getView(int position, View convertView, ViewGroup parent) { final Item item = getItem(position); diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/ItemDragListenerTest.java b/packages/DocumentsUI/tests/src/com/android/documentsui/ItemDragListenerTest.java index 924c99bc633c2..37f6532c1a9cd 100644 --- a/packages/DocumentsUI/tests/src/com/android/documentsui/ItemDragListenerTest.java +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/ItemDragListenerTest.java @@ -16,6 +16,7 @@ package com.android.documentsui; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; @@ -29,6 +30,7 @@ import android.view.View; import com.android.documentsui.testing.ClipDatas; import com.android.documentsui.testing.DragEvents; +import com.android.documentsui.testing.TestDrawable; import com.android.documentsui.testing.TestTimer; import com.android.documentsui.testing.Views; @@ -46,6 +48,7 @@ public class ItemDragListenerTest { private static final long DELAY_AFTER_HOVERING = ItemDragListener.SPRING_TIMEOUT + 1; private View mTestView; + private TestDrawable mTestBackground; private TestDragHost mTestDragHost; private TestTimer mTestTimer; @@ -54,9 +57,10 @@ public class ItemDragListenerTest { @Before public void setUp() { mTestView = Views.createTestView(); - + mTestBackground = new TestDrawable(); mTestTimer = new TestTimer(); mTestDragHost = new TestDragHost(); + mListener = new TestDragListener(mTestDragHost, mTestTimer); } @@ -87,6 +91,25 @@ public class ItemDragListenerTest { assertNull(mTestDragHost.mHighlightedView); } + @Test + public void testDragLocation_notCrashWithoutBackground() { + DragEvent locationEvent = DragEvents.createTestLocationEvent(3, 4); + mListener.onDrag(mTestView, locationEvent); + } + + @Test + public void testDragLocation_setHotSpotOnBackground() { + Views.setBackground(mTestView, mTestBackground); + + final float x = 2; + final float y = 4; + DragEvent locationEvent = DragEvents.createTestLocationEvent(x, y); + mListener.onDrag(mTestView, locationEvent); + + assertEquals(x, mTestBackground.hotspotX, 0); + assertEquals(y, mTestBackground.hotspotY, 0); + } + @Test public void testHover_OpensView() { triggerDragEvent(DragEvent.ACTION_DRAG_ENTERED); diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/testing/DragEvents.java b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/DragEvents.java index 1a009a44c5cbb..4ad9ec0701c2e 100644 --- a/packages/DocumentsUI/tests/src/com/android/documentsui/testing/DragEvents.java +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/DragEvents.java @@ -32,6 +32,14 @@ public final class DragEvents { return mockEvent; } + public static DragEvent createTestLocationEvent(float x, float y) { + final DragEvent locationEvent = createTestDragEvent(DragEvent.ACTION_DRAG_LOCATION); + Mockito.when(locationEvent.getX()).thenReturn(x); + Mockito.when(locationEvent.getY()).thenReturn(y); + + return locationEvent; + } + public static DragEvent createTestDropEvent(ClipData clipData) { final DragEvent dropEvent = createTestDragEvent(DragEvent.ACTION_DROP); Mockito.when(dropEvent.getClipData()).thenReturn(clipData); diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestDrawable.java b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestDrawable.java new file mode 100644 index 0000000000000..bc3831ec5089d --- /dev/null +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/TestDrawable.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.documentsui.testing; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.drawable.Drawable; + +public class TestDrawable extends Drawable { + + public float hotspotX; + public float hotspotY; + + @Override + public void setHotspot(float x, float y) { + hotspotX = x; + hotspotY = y; + } + + @Override + public void draw(Canvas canvas) { + throw new UnsupportedOperationException(); + } + + @Override + public void setAlpha(int alpha) { + throw new UnsupportedOperationException(); + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + throw new UnsupportedOperationException(); + } + + @Override + public int getOpacity() { + throw new UnsupportedOperationException(); + } +} diff --git a/packages/DocumentsUI/tests/src/com/android/documentsui/testing/Views.java b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/Views.java index 15aa01b3e3c67..52a9cbc19d133 100644 --- a/packages/DocumentsUI/tests/src/com/android/documentsui/testing/Views.java +++ b/packages/DocumentsUI/tests/src/com/android/documentsui/testing/Views.java @@ -16,6 +16,7 @@ package com.android.documentsui.testing; +import android.graphics.drawable.Drawable; import android.view.View; import org.mockito.Mockito; @@ -31,4 +32,8 @@ public final class Views { return view; } + + public static void setBackground(View testView, Drawable background) { + Mockito.when(testView.getBackground()).thenReturn(background); + } }