From 5621909c288e785ff9c8a6cc12d3dae8ed837b66 Mon Sep 17 00:00:00 2001 From: calderwoodra Date: Fri, 13 Apr 2018 14:58:17 -0700 Subject: Implement drag to remove contacts in SpeedDialFragment. Bug: 36841782 Test: manual PiperOrigin-RevId: 192828773 Change-Id: Id9066346e6b2a03f672ce3ad11027f15adfbb7e6 --- java/com/android/dialer/speeddial/ContextMenu.java | 4 + .../dialer/speeddial/FavoritesViewHolder.java | 24 ++++- .../android/dialer/speeddial/SpeedDialAdapter.java | 76 +++++++++------ .../dialer/speeddial/SpeedDialFragment.java | 41 +++++++- ...peedDialFavoritesViewHolderOnTouchListener.java | 107 +++++++++++++++++++++ .../SpeedDialItemTouchHelperCallback.java | 83 ++++++++++++++++ .../draghelper/SpeedDialLayoutManager.java | 40 ++++++++ 7 files changed, 338 insertions(+), 37 deletions(-) create mode 100644 java/com/android/dialer/speeddial/draghelper/SpeedDialFavoritesViewHolderOnTouchListener.java create mode 100644 java/com/android/dialer/speeddial/draghelper/SpeedDialItemTouchHelperCallback.java create mode 100644 java/com/android/dialer/speeddial/draghelper/SpeedDialLayoutManager.java (limited to 'java/com/android/dialer/speeddial') diff --git a/java/com/android/dialer/speeddial/ContextMenu.java b/java/com/android/dialer/speeddial/ContextMenu.java index a7fa65556..b6ac98862 100644 --- a/java/com/android/dialer/speeddial/ContextMenu.java +++ b/java/com/android/dialer/speeddial/ContextMenu.java @@ -81,6 +81,10 @@ public class ContextMenu extends LinearLayout { } } + public boolean isVisible() { + return getVisibility() == View.VISIBLE; + } + /** Listener to report user clicks on menu items. */ public interface ContextMenuItemListener { diff --git a/java/com/android/dialer/speeddial/FavoritesViewHolder.java b/java/com/android/dialer/speeddial/FavoritesViewHolder.java index 4f0cf65a0..56d9f36d9 100644 --- a/java/com/android/dialer/speeddial/FavoritesViewHolder.java +++ b/java/com/android/dialer/speeddial/FavoritesViewHolder.java @@ -19,9 +19,11 @@ package com.android.dialer.speeddial; import android.content.Context; import android.provider.ContactsContract.Contacts; import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.helper.ItemTouchHelper; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; +import android.view.ViewConfiguration; import android.widget.FrameLayout; import android.widget.QuickContactBadge; import android.widget.TextView; @@ -29,12 +31,14 @@ import com.android.dialer.common.Assert; import com.android.dialer.glidephotomanager.GlidePhotoManagerComponent; import com.android.dialer.glidephotomanager.PhotoInfo; import com.android.dialer.speeddial.database.SpeedDialEntry.Channel; +import com.android.dialer.speeddial.draghelper.SpeedDialFavoritesViewHolderOnTouchListener; +import com.android.dialer.speeddial.draghelper.SpeedDialFavoritesViewHolderOnTouchListener.OnTouchFinishCallback; import com.android.dialer.speeddial.loader.SpeedDialUiItem; import java.util.List; /** ViewHolder for starred/favorite contacts in {@link SpeedDialFragment}. */ public class FavoritesViewHolder extends RecyclerView.ViewHolder - implements OnClickListener, OnLongClickListener { + implements OnClickListener, OnLongClickListener, OnTouchFinishCallback { private final FavoriteContactsListener listener; @@ -45,7 +49,7 @@ public class FavoritesViewHolder extends RecyclerView.ViewHolder private SpeedDialUiItem speedDialUiItem; - public FavoritesViewHolder(View view, FavoriteContactsListener listener) { + public FavoritesViewHolder(View view, ItemTouchHelper helper, FavoriteContactsListener listener) { super(view); photoView = view.findViewById(R.id.avatar); nameView = view.findViewById(R.id.name); @@ -53,6 +57,9 @@ public class FavoritesViewHolder extends RecyclerView.ViewHolder videoCallIcon = view.findViewById(R.id.video_call_container); view.setOnClickListener(this); view.setOnLongClickListener(this); + view.setOnTouchListener( + new SpeedDialFavoritesViewHolderOnTouchListener( + ViewConfiguration.get(view.getContext()), helper, this, this)); photoView.setClickable(false); this.listener = listener; } @@ -96,12 +103,16 @@ public class FavoritesViewHolder extends RecyclerView.ViewHolder @Override public boolean onLongClick(View view) { - // TODO(calderwoodra): implement drag and drop logic // TODO(calderwoodra): add bounce/sin wave scale animation - listener.onLongClick(photoView, speedDialUiItem); + listener.showContextMenu(photoView, speedDialUiItem); return true; } + @Override + public void onTouchFinished(boolean closeContextMenu) { + listener.onTouchFinished(closeContextMenu); + } + /** Listener/callback for {@link FavoritesViewHolder} actions. */ public interface FavoriteContactsListener { @@ -112,6 +123,9 @@ public class FavoritesViewHolder extends RecyclerView.ViewHolder void onClick(Channel channel); /** Called when the user long clicks on a favorite contact. */ - void onLongClick(View view, SpeedDialUiItem speedDialUiItem); + void showContextMenu(View view, SpeedDialUiItem speedDialUiItem); + + /** Called when the user is no longer touching the favorite contact. */ + void onTouchFinished(boolean closeContextMenu); } } diff --git a/java/com/android/dialer/speeddial/SpeedDialAdapter.java b/java/com/android/dialer/speeddial/SpeedDialAdapter.java index 3312397c7..6f6ac5498 100644 --- a/java/com/android/dialer/speeddial/SpeedDialAdapter.java +++ b/java/com/android/dialer/speeddial/SpeedDialAdapter.java @@ -21,12 +21,10 @@ import android.content.Context; import android.os.Build.VERSION_CODES; import android.support.annotation.IntDef; import android.support.annotation.NonNull; -import android.support.annotation.VisibleForTesting; -import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.GridLayoutManager.SpanSizeLookup; import android.support.v7.widget.RecyclerView; -import android.support.v7.widget.RecyclerView.LayoutManager; import android.support.v7.widget.RecyclerView.ViewHolder; +import android.support.v7.widget.helper.ItemTouchHelper; import android.util.ArrayMap; import android.view.LayoutInflater; import android.view.ViewGroup; @@ -34,10 +32,12 @@ import com.android.dialer.common.Assert; import com.android.dialer.speeddial.FavoritesViewHolder.FavoriteContactsListener; import com.android.dialer.speeddial.HeaderViewHolder.SpeedDialHeaderListener; import com.android.dialer.speeddial.SuggestionViewHolder.SuggestedContactsListener; +import com.android.dialer.speeddial.draghelper.SpeedDialItemTouchHelperCallback.ItemTouchHelperAdapter; import com.android.dialer.speeddial.loader.SpeedDialUiItem; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -55,7 +55,8 @@ import java.util.Map; */ @SuppressWarnings("AndroidApiChecker") @TargetApi(VERSION_CODES.N) -public final class SpeedDialAdapter extends RecyclerView.Adapter { +public final class SpeedDialAdapter extends RecyclerView.Adapter + implements ItemTouchHelperAdapter { @Retention(RetentionPolicy.SOURCE) @IntDef({RowType.STARRED_HEADER, RowType.SUGGESTION_HEADER, RowType.STARRED, RowType.SUGGESTION}) @@ -74,6 +75,9 @@ public final class SpeedDialAdapter extends RecyclerView.Adapter positionToRowTypeMap = new ArrayMap<>(); private List speedDialUiItems; + // Needed for FavoriteViewHolder + private ItemTouchHelper itemTouchHelper; + public SpeedDialAdapter( Context context, FavoriteContactsListener favoritesListener, @@ -97,7 +101,9 @@ public final class SpeedDialAdapter extends RecyclerView.Adapter toPosition; i--) { + Collections.swap(speedDialUiItems, i, i - 1); + } } + // TODO(calderwoodra): store pinned positions + notifyItemMoved(fromPosition, toPosition); + } + + @Override + public boolean canDropOver(ViewHolder target) { + return target instanceof FavoritesViewHolder; + } + + public void setItemTouchHelper(ItemTouchHelper itemTouchHelper) { + this.itemTouchHelper = itemTouchHelper; } } diff --git a/java/com/android/dialer/speeddial/SpeedDialFragment.java b/java/com/android/dialer/speeddial/SpeedDialFragment.java index d323b1bb4..b58d4abf4 100644 --- a/java/com/android/dialer/speeddial/SpeedDialFragment.java +++ b/java/com/android/dialer/speeddial/SpeedDialFragment.java @@ -21,6 +21,7 @@ import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.helper.ItemTouchHelper; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -36,6 +37,8 @@ import com.android.dialer.speeddial.FavoritesViewHolder.FavoriteContactsListener import com.android.dialer.speeddial.HeaderViewHolder.SpeedDialHeaderListener; import com.android.dialer.speeddial.SuggestionViewHolder.SuggestedContactsListener; import com.android.dialer.speeddial.database.SpeedDialEntry.Channel; +import com.android.dialer.speeddial.draghelper.SpeedDialItemTouchHelperCallback; +import com.android.dialer.speeddial.draghelper.SpeedDialLayoutManager; import com.android.dialer.speeddial.loader.SpeedDialUiItem; import com.android.dialer.speeddial.loader.UiItemLoaderComponent; import com.google.common.collect.ImmutableList; @@ -60,8 +63,10 @@ public class SpeedDialFragment extends Fragment { private View rootLayout; private ContextMenu contextMenu; private FrameLayout contextMenuBackground; - private SpeedDialAdapter adapter; private ContextMenuItemListener contextMenuItemListener; + + private SpeedDialAdapter adapter; + private SpeedDialLayoutManager layoutManager; private SupportUiListener> speedDialLoaderListener; public static SpeedDialFragment newInstance() { @@ -74,13 +79,23 @@ public class SpeedDialFragment extends Fragment { LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { LogUtil.enterBlock("SpeedDialFragment.onCreateView"); rootLayout = inflater.inflate(R.layout.fragment_speed_dial, container, false); - RecyclerView recyclerView = rootLayout.findViewById(R.id.speed_dial_recycler_view); + // Setup our RecyclerView + RecyclerView recyclerView = rootLayout.findViewById(R.id.speed_dial_recycler_view); adapter = new SpeedDialAdapter(getContext(), favoritesListener, suggestedListener, headerListener); - recyclerView.setLayoutManager(adapter.getLayoutManager(getContext())); + layoutManager = new SpeedDialLayoutManager(getContext(), 3 /* spanCount */); + layoutManager.setSpanSizeLookup(adapter.getSpanSizeLookup()); + recyclerView.setLayoutManager(layoutManager); recyclerView.setAdapter(adapter); + // Setup drag and drop touch helper + ItemTouchHelper.Callback callback = new SpeedDialItemTouchHelperCallback(adapter); + ItemTouchHelper touchHelper = new ItemTouchHelper(callback); + touchHelper.attachToRecyclerView(recyclerView); + adapter.setItemTouchHelper(touchHelper); + + // Setup favorite contact context menu contextMenu = rootLayout.findViewById(R.id.favorite_contact_context_menu); contextMenuBackground = rootLayout.findViewById(R.id.context_menu_background); contextMenuBackground.setOnClickListener( @@ -141,10 +156,26 @@ public class SpeedDialFragment extends Fragment { } @Override - public void onLongClick(View view, SpeedDialUiItem speedDialUiItem) { - contextMenuBackground.setVisibility(View.VISIBLE); + public void showContextMenu(View view, SpeedDialUiItem speedDialUiItem) { + layoutManager.setScrollEnabled(false); contextMenu.showMenu(rootLayout, view, speedDialUiItem, contextMenuItemListener); } + + @Override + public void onTouchFinished(boolean closeContextMenu) { + layoutManager.setScrollEnabled(true); + + if (closeContextMenu) { + contextMenu.hideMenu(); + } else if (contextMenu.isVisible()) { + // If we're showing the context menu, show this background surface so that we can intercept + // touch events to close the menu + // Note: We call this in onTouchFinished because if we show the background before the user + // is done, they might try to drag the view and but won't be able to because this view would + // intercept all of the touch events. + contextMenuBackground.setVisibility(View.VISIBLE); + } + } } private final class SpeedDialSuggestedListener implements SuggestedContactsListener { diff --git a/java/com/android/dialer/speeddial/draghelper/SpeedDialFavoritesViewHolderOnTouchListener.java b/java/com/android/dialer/speeddial/draghelper/SpeedDialFavoritesViewHolderOnTouchListener.java new file mode 100644 index 000000000..00fecd788 --- /dev/null +++ b/java/com/android/dialer/speeddial/draghelper/SpeedDialFavoritesViewHolderOnTouchListener.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2018 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.dialer.speeddial.draghelper; + +import android.support.v7.widget.RecyclerView.ViewHolder; +import android.support.v7.widget.helper.ItemTouchHelper; +import android.view.HapticFeedbackConstants; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnTouchListener; +import android.view.ViewConfiguration; +import com.android.dialer.common.Assert; + +/** OnTouchListener for the {@link com.android.dialer.speeddial.FavoritesViewHolder}. */ +public class SpeedDialFavoritesViewHolderOnTouchListener implements OnTouchListener { + + private final ViewConfiguration configuration; + private final ItemTouchHelper itemTouchHelper; + private final ViewHolder viewHolder; + private final OnTouchFinishCallback onTouchFinishCallback; + + private boolean hasPerformedLongClick; + private float startX; + private float startY; + + public SpeedDialFavoritesViewHolderOnTouchListener( + ViewConfiguration configuration, + ItemTouchHelper itemTouchHelper, + ViewHolder viewHolder, + OnTouchFinishCallback onTouchFinishCallback) { + this.configuration = configuration; + this.itemTouchHelper = itemTouchHelper; + this.viewHolder = viewHolder; + this.onTouchFinishCallback = onTouchFinishCallback; + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + startX = event.getX(); + startY = event.getY(); + return true; + case MotionEvent.ACTION_MOVE: + // If the user has long clicked the view + if (event.getEventTime() - event.getDownTime() > ViewConfiguration.getLongPressTimeout()) { + // Perform long click if we haven't already + if (!hasPerformedLongClick) { + v.performLongClick(); + v.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + hasPerformedLongClick = true; + } else if (moveEventExceedsTouchSlop(event)) { + itemTouchHelper.startDrag(viewHolder); + onTouchFinishCallback.onTouchFinished(true); + } + } + return true; + case MotionEvent.ACTION_UP: + if (event.getEventTime() - event.getDownTime() < ViewConfiguration.getLongPressTimeout()) { + v.performClick(); + } + // fallthrough + case MotionEvent.ACTION_CANCEL: + hasPerformedLongClick = false; + onTouchFinishCallback.onTouchFinished(false); + return true; + default: + return false; + } + } + + private boolean moveEventExceedsTouchSlop(MotionEvent event) { + Assert.checkArgument(event.getAction() == MotionEvent.ACTION_MOVE); + if (event.getHistorySize() <= 0) { + return false; + } + + return Math.abs(startX - event.getX()) > configuration.getScaledTouchSlop() + || Math.abs(startY - event.getY()) > configuration.getScaledTouchSlop(); + } + + /** Callback to listen for on touch events ending. */ + public interface OnTouchFinishCallback { + + /** + * Called when the user stops touching the view. + * + * @see MotionEvent#ACTION_UP + * @see MotionEvent#ACTION_CANCEL + */ + void onTouchFinished(boolean closeContextMenu); + } +} diff --git a/java/com/android/dialer/speeddial/draghelper/SpeedDialItemTouchHelperCallback.java b/java/com/android/dialer/speeddial/draghelper/SpeedDialItemTouchHelperCallback.java new file mode 100644 index 000000000..d1d9f478b --- /dev/null +++ b/java/com/android/dialer/speeddial/draghelper/SpeedDialItemTouchHelperCallback.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2018 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.dialer.speeddial.draghelper; + +import android.support.annotation.NonNull; +import android.support.v7.widget.RecyclerView; +import android.support.v7.widget.RecyclerView.ViewHolder; +import android.support.v7.widget.helper.ItemTouchHelper; + +/** {@link ItemTouchHelper} for Speed Dial favorite contacts. */ +public class SpeedDialItemTouchHelperCallback extends ItemTouchHelper.Callback { + + private final ItemTouchHelperAdapter adapter; + + public SpeedDialItemTouchHelperCallback(ItemTouchHelperAdapter adapter) { + this.adapter = adapter; + } + + @Override + public boolean isLongPressDragEnabled() { + // We'll manually call ItemTouchHelper#startDrag + return false; + } + + @Override + public boolean isItemViewSwipeEnabled() { + // We don't want to enable swiping + return false; + } + + @Override + public boolean canDropOver( + @NonNull RecyclerView recyclerView, @NonNull ViewHolder current, @NonNull ViewHolder target) { + return adapter.canDropOver(target); + } + + @Override + public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull ViewHolder viewHolder) { + if (!adapter.canDropOver(viewHolder)) { + return makeMovementFlags(0, 0); + } + + int dragFlags = + ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.START | ItemTouchHelper.END; + return makeMovementFlags(dragFlags, /* swipeFlags */ 0); + } + + @Override + public boolean onMove( + @NonNull RecyclerView recyclerView, + @NonNull ViewHolder viewHolder, + @NonNull ViewHolder target) { + adapter.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition()); + return true; + } + + @Override + public void onSwiped(@NonNull ViewHolder viewHolder, int direction) { + // No-op since we don't support swiping + } + + /** RecyclerView adapters interested in drag and drop should implement this interface. */ + public interface ItemTouchHelperAdapter { + + void onItemMove(int fromPosition, int toPosition); + + boolean canDropOver(ViewHolder target); + } +} diff --git a/java/com/android/dialer/speeddial/draghelper/SpeedDialLayoutManager.java b/java/com/android/dialer/speeddial/draghelper/SpeedDialLayoutManager.java new file mode 100644 index 000000000..fcc925186 --- /dev/null +++ b/java/com/android/dialer/speeddial/draghelper/SpeedDialLayoutManager.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2017 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.dialer.speeddial.draghelper; + +import android.content.Context; +import android.support.v7.widget.GridLayoutManager; + +/** {@link GridLayoutManager} that allows disabling scrolling. */ +public class SpeedDialLayoutManager extends GridLayoutManager { + + private boolean isScrollEnabled = true; + + public SpeedDialLayoutManager(Context context, int spanCount) { + super(context, spanCount); + } + + public void setScrollEnabled(boolean flag) { + this.isScrollEnabled = flag; + } + + @Override + public boolean canScrollVertically() { + // Similarly you can customize "canScrollHorizontally()" for managing horizontal scroll + return isScrollEnabled && super.canScrollVertically(); + } +} -- cgit v1.2.3