1 /* 2 * Copyright (C) 2016 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.settings.localepicker; 18 19 import android.content.Context; 20 import android.graphics.Canvas; 21 import android.os.Bundle; 22 import android.os.LocaleList; 23 import android.support.v4.view.MotionEventCompat; 24 import android.support.v7.widget.RecyclerView; 25 import android.support.v7.widget.helper.ItemTouchHelper; 26 import android.util.Log; 27 import android.util.TypedValue; 28 import android.view.LayoutInflater; 29 import android.view.MotionEvent; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.widget.CompoundButton; 33 34 import com.android.internal.app.LocalePicker; 35 import com.android.internal.app.LocaleStore; 36 37 import com.android.settings.CreateShortcut; 38 import com.android.settings.R; 39 40 import java.text.NumberFormat; 41 import java.util.ArrayList; 42 import java.util.List; 43 import java.util.Locale; 44 45 46 class LocaleDragAndDropAdapter 47 extends RecyclerView.Adapter<LocaleDragAndDropAdapter.CustomViewHolder> { 48 49 private static final String TAG = "LocaleDragAndDropAdapter"; 50 private static final String CFGKEY_SELECTED_LOCALES = "selectedLocales"; 51 private final Context mContext; 52 private final List<LocaleStore.LocaleInfo> mFeedItemList; 53 private final ItemTouchHelper mItemTouchHelper; 54 private RecyclerView mParentView = null; 55 private boolean mRemoveMode = false; 56 private boolean mDragEnabled = true; 57 private NumberFormat mNumberFormatter = NumberFormat.getNumberInstance(); 58 59 class CustomViewHolder extends RecyclerView.ViewHolder implements View.OnTouchListener { 60 private final LocaleDragCell mLocaleDragCell; 61 62 public CustomViewHolder(LocaleDragCell view) { 63 super(view); 64 mLocaleDragCell = view; 65 mLocaleDragCell.getDragHandle().setOnTouchListener(this); 66 } 67 68 public LocaleDragCell getLocaleDragCell() { 69 return mLocaleDragCell; 70 } 71 72 @Override 73 public boolean onTouch(View v, MotionEvent event) { 74 if (mDragEnabled) { 75 switch (MotionEventCompat.getActionMasked(event)) { 76 case MotionEvent.ACTION_DOWN: 77 mItemTouchHelper.startDrag(this); 78 } 79 } 80 return false; 81 } 82 } 83 84 public LocaleDragAndDropAdapter(Context context, List<LocaleStore.LocaleInfo> feedItemList) { 85 this.mFeedItemList = feedItemList; 86 87 this.mContext = context; 88 89 final float dragElevation = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, 90 context.getResources().getDisplayMetrics()); 91 92 this.mItemTouchHelper = new ItemTouchHelper(new ItemTouchHelper.SimpleCallback( 93 ItemTouchHelper.UP | ItemTouchHelper.DOWN, 0 /* no swipe */) { 94 95 @Override 96 public boolean onMove(RecyclerView view, RecyclerView.ViewHolder source, 97 RecyclerView.ViewHolder target) { 98 onItemMove(source.getAdapterPosition(), target.getAdapterPosition()); 99 return true; 100 } 101 102 @Override 103 public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) { 104 // Swipe is disabled, this is intentionally empty. 105 } 106 107 private static final int SELECTION_GAINED = 1; 108 private static final int SELECTION_LOST = 0; 109 private static final int SELECTION_UNCHANGED = -1; 110 private int mSelectionStatus = SELECTION_UNCHANGED; 111 112 @Override 113 public void onChildDraw(Canvas c, RecyclerView recyclerView, 114 RecyclerView.ViewHolder viewHolder, float dX, float dY, 115 int actionState, boolean isCurrentlyActive) { 116 117 super.onChildDraw(c, recyclerView, viewHolder, dX, dY, 118 actionState, isCurrentlyActive); 119 // We change the elevation if selection changed 120 if (mSelectionStatus != SELECTION_UNCHANGED) { 121 viewHolder.itemView.setElevation( 122 mSelectionStatus == SELECTION_GAINED ? dragElevation : 0); 123 mSelectionStatus = SELECTION_UNCHANGED; 124 } 125 } 126 127 @Override 128 public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) { 129 super.onSelectedChanged(viewHolder, actionState); 130 if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) { 131 mSelectionStatus = SELECTION_GAINED; 132 } else if (actionState == ItemTouchHelper.ACTION_STATE_IDLE) { 133 mSelectionStatus = SELECTION_LOST; 134 } 135 } 136 }); 137 } 138 139 public void setRecyclerView(RecyclerView rv) { 140 mParentView = rv; 141 mItemTouchHelper.attachToRecyclerView(rv); 142 } 143 144 @Override 145 public CustomViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) { 146 final LocaleDragCell item = (LocaleDragCell) LayoutInflater.from(mContext) 147 .inflate(R.layout.locale_drag_cell, viewGroup, false); 148 return new CustomViewHolder(item); 149 } 150 151 @Override 152 public void onBindViewHolder(final CustomViewHolder holder, int i) { 153 final LocaleStore.LocaleInfo feedItem = mFeedItemList.get(i); 154 final LocaleDragCell dragCell = holder.getLocaleDragCell(); 155 final String label = feedItem.getFullNameNative(); 156 final String description = feedItem.getFullNameInUiLanguage(); 157 dragCell.setLabelAndDescription(label, description); 158 dragCell.setLocalized(feedItem.isTranslated()); 159 dragCell.setMiniLabel(mNumberFormatter.format(i + 1)); 160 dragCell.setShowCheckbox(mRemoveMode); 161 dragCell.setShowMiniLabel(!mRemoveMode); 162 dragCell.setShowHandle(!mRemoveMode && mDragEnabled); 163 dragCell.setChecked(mRemoveMode ? feedItem.getChecked() : false); 164 dragCell.setTag(feedItem); 165 dragCell.getCheckbox() 166 .setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { 167 @Override 168 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 169 LocaleStore.LocaleInfo feedItem = 170 (LocaleStore.LocaleInfo) dragCell.getTag(); 171 feedItem.setChecked(isChecked); 172 } 173 }); 174 } 175 176 @Override 177 public int getItemCount() { 178 int itemCount = (null != mFeedItemList ? mFeedItemList.size() : 0); 179 if (itemCount < 2 || mRemoveMode) { 180 setDragEnabled(false); 181 } else { 182 setDragEnabled(true); 183 } 184 return itemCount; 185 } 186 187 void onItemMove(int fromPosition, int toPosition) { 188 if (fromPosition >= 0 && toPosition >= 0) { 189 final LocaleStore.LocaleInfo saved = mFeedItemList.get(fromPosition); 190 mFeedItemList.remove(fromPosition); 191 mFeedItemList.add(toPosition, saved); 192 } else { 193 // TODO: It looks like sometimes the RecycleView tries to swap item -1 194 // I did not see it in a while, but if it happens, investigate and file a bug. 195 Log.e(TAG, String.format(Locale.US, 196 "Negative position in onItemMove %d -> %d", fromPosition, toPosition)); 197 } 198 notifyItemChanged(fromPosition); // to update the numbers 199 notifyItemChanged(toPosition); 200 notifyItemMoved(fromPosition, toPosition); 201 // We don't call doTheUpdate() here because this method is called for each item swap. 202 // So if we drag something across several positions it will be called several times. 203 } 204 205 void setRemoveMode(boolean removeMode) { 206 mRemoveMode = removeMode; 207 int itemCount = mFeedItemList.size(); 208 for (int i = 0; i < itemCount; i++) { 209 mFeedItemList.get(i).setChecked(false); 210 notifyItemChanged(i); 211 } 212 } 213 214 boolean isRemoveMode() { 215 return mRemoveMode; 216 } 217 218 void removeItem(int position) { 219 int itemCount = mFeedItemList.size(); 220 if (itemCount <= 1) { 221 return; 222 } 223 if (position < 0 || position >= itemCount) { 224 return; 225 } 226 mFeedItemList.remove(position); 227 notifyDataSetChanged(); 228 } 229 230 void removeChecked() { 231 int itemCount = mFeedItemList.size(); 232 for (int i = itemCount - 1; i >= 0; i--) { 233 if (mFeedItemList.get(i).getChecked()) { 234 mFeedItemList.remove(i); 235 } 236 } 237 notifyDataSetChanged(); 238 doTheUpdate(); 239 } 240 241 int getCheckedCount() { 242 int result = 0; 243 for (LocaleStore.LocaleInfo li : mFeedItemList) { 244 if (li.getChecked()) { 245 result++; 246 } 247 } 248 return result; 249 } 250 251 LocaleStore.LocaleInfo getFirstChecked() { 252 for (LocaleStore.LocaleInfo li : mFeedItemList) { 253 if (li.getChecked()) { 254 return li; 255 } 256 } 257 return null; 258 } 259 260 void addLocale(LocaleStore.LocaleInfo li) { 261 mFeedItemList.add(li); 262 notifyItemInserted(mFeedItemList.size() - 1); 263 doTheUpdate(); 264 } 265 266 public void doTheUpdate() { 267 int count = mFeedItemList.size(); 268 final Locale[] newList = new Locale[count]; 269 270 for (int i = 0; i < count; i++) { 271 final LocaleStore.LocaleInfo li = mFeedItemList.get(i); 272 newList[i] = li.getLocale(); 273 } 274 275 final LocaleList ll = new LocaleList(newList); 276 updateLocalesWhenAnimationStops(ll); 277 } 278 279 private LocaleList mLocalesToSetNext = null; 280 private LocaleList mLocalesSetLast = null; 281 282 public void updateLocalesWhenAnimationStops(final LocaleList localeList) { 283 if (localeList.equals(mLocalesToSetNext)) { 284 return; 285 } 286 287 // This will only update the Settings application to make things feel more responsive, 288 // the system will be updated later, when animation stopped. 289 LocaleList.setDefault(localeList); 290 291 mLocalesToSetNext = localeList; 292 final RecyclerView.ItemAnimator itemAnimator = mParentView.getItemAnimator(); 293 itemAnimator.isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() { 294 @Override 295 public void onAnimationsFinished() { 296 if (mLocalesToSetNext == null || mLocalesToSetNext.equals(mLocalesSetLast)) { 297 // All animations finished, but the locale list did not change 298 return; 299 } 300 301 LocalePicker.updateLocales(mLocalesToSetNext); 302 mLocalesSetLast = mLocalesToSetNext; 303 new CreateShortcut.ShortcutsUpdateTask(mContext).execute(); 304 305 mLocalesToSetNext = null; 306 307 mNumberFormatter = NumberFormat.getNumberInstance(Locale.getDefault()); 308 } 309 }); 310 } 311 312 private void setDragEnabled(boolean enabled) { 313 mDragEnabled = enabled; 314 } 315 316 /** 317 * Saves the list of checked locales to preserve status when the list is destroyed. 318 * (for instance when the device is rotated) 319 * @param outInstanceState Bundle in which to place the saved state 320 */ 321 public void saveState(Bundle outInstanceState) { 322 if (outInstanceState != null) { 323 final ArrayList<String> selectedLocales = new ArrayList<>(); 324 for (LocaleStore.LocaleInfo li : mFeedItemList) { 325 if (li.getChecked()) { 326 selectedLocales.add(li.getId()); 327 } 328 } 329 outInstanceState.putStringArrayList(CFGKEY_SELECTED_LOCALES, selectedLocales); 330 } 331 } 332 333 /** 334 * Restores the list of checked locales to preserve status when the list is recreated. 335 * (for instance when the device is rotated) 336 * @param savedInstanceState Bundle with the data saved by {@link #saveState(Bundle)} 337 */ 338 public void restoreState(Bundle savedInstanceState) { 339 if (savedInstanceState != null && mRemoveMode) { 340 final ArrayList<String> selectedLocales = 341 savedInstanceState.getStringArrayList(CFGKEY_SELECTED_LOCALES); 342 if (selectedLocales == null || selectedLocales.isEmpty()) { 343 return; 344 } 345 for (LocaleStore.LocaleInfo li : mFeedItemList) { 346 li.setChecked(selectedLocales.contains(li.getId())); 347 } 348 notifyItemRangeChanged(0, mFeedItemList.size()); 349 } 350 } 351 } 352