Suppress auto-closing drawer and add ripple effect on spring load roots.

Bug: 28865182
Change-Id: Ief7967e33b9a0d7e94a667172121d8007f78115b
(cherry picked from commit 17182ca46f7a8f3ae03bb8f5a16116246a5fbd91)
This commit is contained in:
Garfield, Tan
2016-05-27 15:02:35 -07:00
committed by Garfield Tan
parent 92b6768eaa
commit d9ddb3ba5b
13 changed files with 290 additions and 29 deletions

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res/com.android.documentsui"
android:color="?attr/colorControlHighlight">
<item
android:id="@android:id/mask"
android:drawable="@android:color/white"/>
<item>
<selector>
<item
app:state_highlighted="true"
android:drawable="@color/item_doc_background_selected"/>
<item
app:state_highlighted="false"
android:drawable="@android:color/transparent"/>
</selector>
</item>
</ripple>

View File

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

View File

@@ -14,7 +14,8 @@
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<com.android.documentsui.RootItemView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
@@ -22,7 +23,8 @@
android:paddingEnd="@dimen/list_item_padding"
android:gravity="center_vertical"
android:orientation="horizontal"
android:baselineAligned="false">
android:baselineAligned="false"
android:background="@drawable/root_item_background">
<FrameLayout
android:layout_width="@dimen/icon_size"
@@ -68,4 +70,4 @@
</LinearLayout>
</LinearLayout>
</com.android.documentsui.RootItemView>

View File

@@ -17,4 +17,8 @@
<declare-styleable name="DocumentsTheme">
<attr name="colorActionMode" format="color"/>
</declare-styleable>
<declare-styleable name="RootItemView">
<attr name="state_highlighted" format="boolean"/>
</declare-styleable>
</resources>

View File

@@ -17,4 +17,5 @@
<resources>
<item name="drag_hovering_tag" type="id" />
<item name="item_position_tag" type="id" />
<item name="layout_id_tag" type="id" />
</resources>

View File

@@ -256,8 +256,6 @@ public abstract class BaseActivity extends Activity
} else {
new PickRootTask(this, root).executeOnExecutor(getExecutorForCurrentDirectory());
}
mNavigator.revealRootsDrawer(false);
}
@Override

View File

@@ -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<H extends DragHost> 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<H extends DragHost> 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);

View File

@@ -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);
}
}

View File

@@ -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<Item> {
private static final Map<String, Long> sIdMap = new HashMap<String, Long>();
// 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);

View File

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

View File

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

View File

@@ -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();
}
}

View File

@@ -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);
}
}