diff options
author | Treehugger Robot <treehugger-gerrit@google.com> | 2018-04-13 23:31:31 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2018-04-13 23:31:31 +0000 |
commit | d94c00d6629da03c96a101781fbc21e0daca8db8 (patch) | |
tree | a59d481b856647dd26a0aefd0ea3be8e82784303 /java/com/android/dialer/speeddial | |
parent | 91004eccd1f2c95ac85f4ab15ba0d6b2aaaaac5b (diff) | |
parent | 5621909c288e785ff9c8a6cc12d3dae8ed837b66 (diff) |
Merge "Implement drag to remove contacts in SpeedDialFragment."
Diffstat (limited to 'java/com/android/dialer/speeddial')
7 files changed, 338 insertions, 37 deletions
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<RecyclerView.ViewHolder> { +public final class SpeedDialAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> + 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<RecyclerView.Vi private final Map<Integer, Integer> positionToRowTypeMap = new ArrayMap<>(); private List<SpeedDialUiItem> speedDialUiItems; + // Needed for FavoriteViewHolder + private ItemTouchHelper itemTouchHelper; + public SpeedDialAdapter( Context context, FavoriteContactsListener favoritesListener, @@ -97,7 +101,9 @@ public final class SpeedDialAdapter extends RecyclerView.Adapter<RecyclerView.Vi switch (viewType) { case RowType.STARRED: return new FavoritesViewHolder( - inflater.inflate(R.layout.favorite_item_layout, parent, false), favoritesListener); + inflater.inflate(R.layout.favorite_item_layout, parent, false), + itemTouchHelper, + favoritesListener); case RowType.SUGGESTION: return new SuggestionViewHolder( inflater.inflate(R.layout.suggestion_row_layout, parent, false), suggestedListener); @@ -162,30 +168,46 @@ public final class SpeedDialAdapter extends RecyclerView.Adapter<RecyclerView.Vi } } - /* package-private */ LayoutManager getLayoutManager(Context context) { - GridLayoutManager layoutManager = new GridLayoutManager(context, 3 /* spanCount */); - layoutManager.setSpanSizeLookup( - new SpanSizeLookup() { - @Override - public int getSpanSize(int position) { - return SpeedDialAdapter.this.getSpanSize(position); - } - }); - return layoutManager; + public SpanSizeLookup getSpanSizeLookup() { + return new SpanSizeLookup() { + @Override + public int getSpanSize(int position) { + switch (getItemViewType(position)) { + case RowType.SUGGESTION: + case RowType.STARRED_HEADER: + case RowType.SUGGESTION_HEADER: + return 3; // span the whole screen + case RowType.STARRED: + return 1; // span 1/3 of the screen + default: + throw Assert.createIllegalStateFailException( + "Invalid row type: " + positionToRowTypeMap.get(position)); + } + } + }; } - @VisibleForTesting - int getSpanSize(int position) { - switch (getItemViewType(position)) { - case RowType.SUGGESTION: - case RowType.STARRED_HEADER: - case RowType.SUGGESTION_HEADER: - return 3; // span the whole screen - case RowType.STARRED: - return 1; // span 1/3 of the screen - default: - throw Assert.createIllegalStateFailException( - "Invalid row type: " + positionToRowTypeMap.get(position)); + @Override + public void onItemMove(int fromPosition, int toPosition) { + if (fromPosition < toPosition) { + for (int i = fromPosition; i < toPosition && i < speedDialUiItems.size() - 1; i++) { + Collections.swap(speedDialUiItems, i, i + 1); + } + } else { + for (int i = fromPosition - 1; i > 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<ImmutableList<SpeedDialUiItem>> 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(); + } +} |