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