Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 5621909c authored by calderwoodra's avatar calderwoodra Committed by Copybara-Service
Browse files

Implement drag to remove contacts in SpeedDialFragment.

Bug: 36841782
Test: manual
PiperOrigin-RevId: 192828773
Change-Id: Id9066346e6b2a03f672ce3ad11027f15adfbb7e6
parent 0d0afb36
Loading
Loading
Loading
Loading
+4 −0
Original line number Diff line number Diff line
@@ -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 {

+19 −5
Original line number Diff line number Diff line
@@ -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);
  }
}
+49 −27
Original line number Diff line number Diff line
@@ -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,20 +168,10 @@ 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() {
  public SpanSizeLookup getSpanSizeLookup() {
    return new SpanSizeLookup() {
      @Override
      public int getSpanSize(int position) {
            return SpeedDialAdapter.this.getSpanSize(position);
          }
        });
    return layoutManager;
  }

  @VisibleForTesting
  int getSpanSize(int position) {
        switch (getItemViewType(position)) {
          case RowType.SUGGESTION:
          case RowType.STARRED_HEADER:
@@ -188,4 +184,30 @@ public final class SpeedDialAdapter extends RecyclerView.Adapter<RecyclerView.Vi
                "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;
  }
}
+36 −5
Original line number Diff line number Diff line
@@ -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 {
+107 −0
Original line number Diff line number Diff line
/*
 * 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);
  }
}
Loading