1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 17 package com.android.dialer.speeddial; 18 19 import android.content.Context; 20 import android.support.annotation.IntDef; 21 import android.support.annotation.NonNull; 22 import android.support.annotation.Nullable; 23 import android.support.v7.widget.GridLayoutManager.SpanSizeLookup; 24 import android.support.v7.widget.RecyclerView; 25 import android.support.v7.widget.RecyclerView.ViewHolder; 26 import android.support.v7.widget.helper.ItemTouchHelper; 27 import android.util.ArrayMap; 28 import android.view.LayoutInflater; 29 import android.view.ViewGroup; 30 import android.view.animation.AnticipateInterpolator; 31 import android.widget.FrameLayout; 32 import com.android.dialer.common.Assert; 33 import com.android.dialer.speeddial.FavoritesViewHolder.FavoriteContactsListener; 34 import com.android.dialer.speeddial.HeaderViewHolder.SpeedDialHeaderListener; 35 import com.android.dialer.speeddial.SpeedDialFragment.HostInterface; 36 import com.android.dialer.speeddial.SuggestionViewHolder.SuggestedContactsListener; 37 import com.android.dialer.speeddial.draghelper.SpeedDialItemTouchHelperCallback.ItemTouchHelperAdapter; 38 import com.android.dialer.speeddial.loader.SpeedDialUiItem; 39 import com.google.common.collect.ImmutableList; 40 import java.lang.annotation.Retention; 41 import java.lang.annotation.RetentionPolicy; 42 import java.util.ArrayList; 43 import java.util.List; 44 import java.util.Map; 45 46 /** 47 * RecyclerView adapter for {@link SpeedDialFragment}. 48 * 49 * <p>Displays a list in the following order: 50 * 51 * <ol> 52 * <li>Favorite contacts header (with add button) 53 * <li>Favorite contacts 54 * <li>Suggested contacts header 55 * <li>Suggested contacts 56 * </ol> 57 */ 58 public final class SpeedDialAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> 59 implements ItemTouchHelperAdapter { 60 61 private static final int NON_CONTACT_ITEM_NUMBER_BEFORE_FAVORITES = 2; 62 private static final int NON_CONTACT_ITEM_NUMBER_BEFORE_SUGGESTION = 3; 63 64 private static final float IN_REMOVE_VIEW_SCALE = 0.5f; 65 private static final float IN_REMOVE_VIEW_ALPHA = 0.5f; 66 67 @Retention(RetentionPolicy.SOURCE) 68 @IntDef({RowType.STARRED_HEADER, RowType.SUGGESTION_HEADER, RowType.STARRED, RowType.SUGGESTION}) 69 @interface RowType { 70 int REMOVE_VIEW = 0; 71 int STARRED_HEADER = 1; 72 int SUGGESTION_HEADER = 2; 73 int STARRED = 3; 74 int SUGGESTION = 4; 75 } 76 77 private final Context context; 78 private final FavoriteContactsListener favoritesListener; 79 private final SuggestedContactsListener suggestedListener; 80 private final SpeedDialHeaderListener headerListener; 81 private final HostInterface hostInterface; 82 83 private final Map<Integer, Integer> positionToRowTypeMap = new ArrayMap<>(); 84 private List<SpeedDialUiItem> speedDialUiItems; 85 86 // Needed for FavoriteViewHolder 87 private ItemTouchHelper itemTouchHelper; 88 89 private RemoveViewHolder removeViewHolder; 90 private FavoritesViewHolder draggingFavoritesViewHolder; 91 SpeedDialAdapter( Context context, FavoriteContactsListener favoritesListener, SuggestedContactsListener suggestedListener, SpeedDialHeaderListener headerListener, HostInterface hostInterface)92 public SpeedDialAdapter( 93 Context context, 94 FavoriteContactsListener favoritesListener, 95 SuggestedContactsListener suggestedListener, 96 SpeedDialHeaderListener headerListener, 97 HostInterface hostInterface) { 98 this.context = context; 99 this.favoritesListener = favoritesListener; 100 this.suggestedListener = suggestedListener; 101 this.headerListener = headerListener; 102 this.hostInterface = hostInterface; 103 } 104 105 @Override getItemViewType(int position)106 public int getItemViewType(int position) { 107 return positionToRowTypeMap.get(position); 108 } 109 110 @NonNull 111 @Override onCreateViewHolder(@onNull ViewGroup parent, int viewType)112 public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 113 LayoutInflater inflater = LayoutInflater.from(context); 114 switch (viewType) { 115 case RowType.STARRED: 116 return new FavoritesViewHolder( 117 inflater.inflate(R.layout.favorite_item_layout, parent, false), 118 itemTouchHelper, 119 favoritesListener); 120 case RowType.SUGGESTION: 121 return new SuggestionViewHolder( 122 inflater.inflate(R.layout.suggestion_row_layout, parent, false), suggestedListener); 123 case RowType.STARRED_HEADER: 124 case RowType.SUGGESTION_HEADER: 125 return new HeaderViewHolder( 126 inflater.inflate(R.layout.speed_dial_header_layout, parent, false), headerListener); 127 case RowType.REMOVE_VIEW: 128 removeViewHolder = 129 new RemoveViewHolder( 130 inflater.inflate(R.layout.favorite_remove_view_layout, parent, false)); 131 return removeViewHolder; 132 default: 133 throw Assert.createIllegalStateFailException("Invalid viewType: " + viewType); 134 } 135 } 136 137 @Override onBindViewHolder(@onNull ViewHolder holder, int position)138 public void onBindViewHolder(@NonNull ViewHolder holder, int position) { 139 switch (getItemViewType(position)) { 140 case RowType.STARRED_HEADER: 141 ((HeaderViewHolder) holder).setHeaderText(R.string.favorites_header); 142 ((HeaderViewHolder) holder).showAddButton(true); 143 return; 144 case RowType.SUGGESTION_HEADER: 145 ((HeaderViewHolder) holder).setHeaderText(R.string.suggestions_header); 146 ((HeaderViewHolder) holder).showAddButton(false); 147 return; 148 case RowType.STARRED: 149 ((FavoritesViewHolder) holder).bind(context, speedDialUiItems.get(position - 2)); 150 // Removed item might come back 151 FrameLayout avatarContainer = ((FavoritesViewHolder) holder).getAvatarContainer(); 152 avatarContainer.setScaleX(1); 153 avatarContainer.setScaleY(1); 154 avatarContainer.setAlpha(1); 155 break; 156 case RowType.SUGGESTION: 157 ((SuggestionViewHolder) holder).bind(context, speedDialUiItems.get(position - 3)); 158 break; 159 case RowType.REMOVE_VIEW: 160 break; 161 default: 162 throw Assert.createIllegalStateFailException("Invalid view holder: " + holder); 163 } 164 } 165 166 @Override getItemCount()167 public int getItemCount() { 168 return positionToRowTypeMap.size(); 169 } 170 setSpeedDialUiItems(List<SpeedDialUiItem> immutableSpeedDialUiItems)171 public void setSpeedDialUiItems(List<SpeedDialUiItem> immutableSpeedDialUiItems) { 172 speedDialUiItems = new ArrayList<>(); 173 speedDialUiItems.addAll(immutableSpeedDialUiItems); 174 speedDialUiItems.sort( 175 (o1, o2) -> { 176 if (o1.isStarred() && o2.isStarred()) { 177 return Integer.compare(o1.pinnedPosition().or(-1), o2.pinnedPosition().or(-1)); 178 } 179 return Boolean.compare(o2.isStarred(), o1.isStarred()); 180 }); 181 updatePositionToRowTypeMap(); 182 } 183 updatePositionToRowTypeMap()184 private void updatePositionToRowTypeMap() { 185 positionToRowTypeMap.clear(); 186 if (speedDialUiItems.isEmpty()) { 187 return; 188 } 189 190 positionToRowTypeMap.put(0, RowType.REMOVE_VIEW); 191 // Show the add favorites even if there are no favorite contacts 192 positionToRowTypeMap.put(1, RowType.STARRED_HEADER); 193 int positionOfSuggestionHeader = NON_CONTACT_ITEM_NUMBER_BEFORE_FAVORITES; 194 for (int i = 0; i < speedDialUiItems.size(); i++) { 195 if (speedDialUiItems.get(i).isStarred()) { 196 positionToRowTypeMap.put(i + NON_CONTACT_ITEM_NUMBER_BEFORE_FAVORITES, RowType.STARRED); 197 positionOfSuggestionHeader++; 198 } else { 199 positionToRowTypeMap.put(i + NON_CONTACT_ITEM_NUMBER_BEFORE_SUGGESTION, RowType.SUGGESTION); 200 } 201 } 202 if (!speedDialUiItems.get(speedDialUiItems.size() - 1).isStarred()) { 203 positionToRowTypeMap.put(positionOfSuggestionHeader, RowType.SUGGESTION_HEADER); 204 } 205 } 206 getSpeedDialUiItems()207 public ImmutableList<SpeedDialUiItem> getSpeedDialUiItems() { 208 if (speedDialUiItems == null || speedDialUiItems.isEmpty()) { 209 return ImmutableList.of(); 210 } 211 return ImmutableList.copyOf(speedDialUiItems); 212 } 213 getSpanSizeLookup()214 public SpanSizeLookup getSpanSizeLookup() { 215 return new SpanSizeLookup() { 216 @Override 217 public int getSpanSize(int position) { 218 switch (getItemViewType(position)) { 219 case RowType.SUGGESTION: 220 case RowType.STARRED_HEADER: 221 case RowType.SUGGESTION_HEADER: 222 case RowType.REMOVE_VIEW: 223 return 3; // span the whole screen 224 case RowType.STARRED: 225 return 1; // span 1/3 of the screen 226 default: 227 throw Assert.createIllegalStateFailException( 228 "Invalid row type: " + positionToRowTypeMap.get(position)); 229 } 230 } 231 }; 232 } 233 234 @Override 235 public void onItemMove(int fromPosition, int toPosition) { 236 if (toPosition == 0) { 237 // drop to removeView 238 return; 239 } 240 // fromPosition/toPosition correspond to adapter position, which is off by 1 from the list 241 // position b/c of the favorites header. So subtract 1 here. 242 speedDialUiItems.add(toPosition - 2, speedDialUiItems.remove(fromPosition - 2)); 243 notifyItemMoved(fromPosition, toPosition); 244 } 245 246 @Override 247 public boolean canDropOver(ViewHolder target) { 248 return target instanceof FavoritesViewHolder || target instanceof RemoveViewHolder; 249 } 250 251 @Override 252 public void onSelectedChanged(@Nullable ViewHolder viewHolder, int actionState) { 253 switch (actionState) { 254 case ItemTouchHelper.ACTION_STATE_DRAG: 255 if (viewHolder != null) { 256 draggingFavoritesViewHolder = (FavoritesViewHolder) viewHolder; 257 draggingFavoritesViewHolder.onSelectedChanged(true); 258 hostInterface.dragFavorite(true); 259 removeViewHolder.show(); 260 } 261 break; 262 case ItemTouchHelper.ACTION_STATE_IDLE: 263 // viewHolder is null in this case 264 if (draggingFavoritesViewHolder != null) { 265 draggingFavoritesViewHolder.onSelectedChanged(false); 266 draggingFavoritesViewHolder = null; 267 hostInterface.dragFavorite(false); 268 removeViewHolder.hide(); 269 } 270 break; 271 default: 272 break; 273 } 274 } 275 276 @Override 277 public void enterRemoveView() { 278 if (draggingFavoritesViewHolder != null) { 279 draggingFavoritesViewHolder 280 .getAvatarContainer() 281 .animate() 282 .scaleX(IN_REMOVE_VIEW_SCALE) 283 .scaleY(IN_REMOVE_VIEW_SCALE) 284 .alpha(IN_REMOVE_VIEW_ALPHA) 285 .start(); 286 } 287 } 288 289 @Override 290 public void leaveRemoveView() { 291 if (draggingFavoritesViewHolder != null) { 292 draggingFavoritesViewHolder 293 .getAvatarContainer() 294 .animate() 295 .scaleX(1) 296 .scaleY(1) 297 .alpha(1) 298 .start(); 299 } 300 } 301 302 @Override 303 public void dropOnRemoveView(ViewHolder fromViewHolder) { 304 if (!(fromViewHolder instanceof FavoritesViewHolder)) { 305 return; 306 } 307 int fromPosition = fromViewHolder.getAdapterPosition(); 308 309 SpeedDialUiItem removedItem = speedDialUiItems.remove(fromPosition - 2); 310 favoritesListener.onRequestRemove(removedItem); 311 ((FavoritesViewHolder) fromViewHolder) 312 .getAvatarContainer() 313 .animate() 314 .scaleX(0) 315 .scaleY(0) 316 .alpha(0) 317 .setInterpolator(new AnticipateInterpolator()) 318 .start(); 319 updatePositionToRowTypeMap(); 320 } 321 322 public void setItemTouchHelper(ItemTouchHelper itemTouchHelper) { 323 this.itemTouchHelper = itemTouchHelper; 324 } 325 326 /** Returns true if there are suggested contacts. */ 327 public boolean hasFrequents() { 328 return !speedDialUiItems.isEmpty() && getItemViewType(getItemCount() - 1) == RowType.SUGGESTION; 329 } 330 } 331