LocaleDragAndDropAdapter.java revision d86cba770d57d169022d5fc230395b5262f4a9fd
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 17package com.android.settings.localepicker; 18 19import android.content.Context; 20import android.graphics.Canvas; 21import android.os.Bundle; 22import android.support.v4.view.MotionEventCompat; 23import android.support.v7.widget.RecyclerView; 24import android.support.v7.widget.helper.ItemTouchHelper; 25import android.util.LocaleList; 26import android.util.Log; 27import android.util.TypedValue; 28import android.view.LayoutInflater; 29import android.view.MotionEvent; 30import android.view.View; 31import android.view.ViewGroup; 32import android.widget.CompoundButton; 33 34import com.android.internal.app.LocalePicker; 35import com.android.internal.app.LocaleStore; 36 37import com.android.settings.R; 38 39import java.text.NumberFormat; 40import java.util.ArrayList; 41import java.util.Collections; 42import java.util.List; 43import java.util.Locale; 44 45 46class 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 private void onItemMove(int fromPosition, int toPosition) { 188 if (fromPosition >= 0 && toPosition >= 0) { 189 Collections.swap(mFeedItemList, fromPosition, toPosition); 190 } else { 191 // TODO: It looks like sometimes the RecycleView tries to swap item -1 192 // Investigate and file a bug. 193 Log.e(TAG, String.format(Locale.US, 194 "Negative position in onItemMove %d -> %d", fromPosition, toPosition)); 195 } 196 notifyItemChanged(fromPosition); // to update the numbers 197 notifyItemChanged(toPosition); 198 notifyItemMoved(fromPosition, toPosition); 199 } 200 201 void setRemoveMode(boolean removeMode) { 202 mRemoveMode = removeMode; 203 int itemCount = mFeedItemList.size(); 204 for (int i = 0; i < itemCount; i++) { 205 mFeedItemList.get(i).setChecked(false); 206 notifyItemChanged(i); 207 } 208 } 209 210 void removeChecked() { 211 int itemCount = mFeedItemList.size(); 212 for (int i = itemCount - 1; i >= 0; i--) { 213 if (mFeedItemList.get(i).getChecked()) { 214 mFeedItemList.remove(i); 215 } 216 } 217 notifyDataSetChanged(); 218 doTheUpdate(); 219 } 220 221 int getCheckedCount() { 222 int result = 0; 223 for (LocaleStore.LocaleInfo li : mFeedItemList) { 224 if (li.getChecked()) { 225 result++; 226 } 227 } 228 return result; 229 } 230 231 LocaleStore.LocaleInfo getFirstChecked() { 232 for (LocaleStore.LocaleInfo li : mFeedItemList) { 233 if (li.getChecked()) { 234 return li; 235 } 236 } 237 return null; 238 } 239 240 void addLocale(LocaleStore.LocaleInfo li) { 241 mFeedItemList.add(li); 242 notifyItemInserted(mFeedItemList.size() - 1); 243 doTheUpdate(); 244 } 245 246 public void doTheUpdate() { 247 int count = mFeedItemList.size(); 248 final Locale[] newList = new Locale[count]; 249 250 for (int i = 0; i < count; i++) { 251 final LocaleStore.LocaleInfo li = mFeedItemList.get(i); 252 newList[i] = li.getLocale(); 253 } 254 255 final LocaleList ll = new LocaleList(newList); 256 updateLocalesWhenAnimationStops(ll); 257 } 258 259 private LocaleList mLocalesToSetNext = null; 260 private LocaleList mLocalesSetLast = null; 261 262 public void updateLocalesWhenAnimationStops(final LocaleList localeList) { 263 if (localeList.equals(mLocalesToSetNext)) { 264 return; 265 } 266 267 // This will only update the Settings application to make things feel more responsive, 268 // the system will be updated later, when animation stopped. 269 LocaleList.setDefault(localeList); 270 271 mLocalesToSetNext = localeList; 272 final RecyclerView.ItemAnimator itemAnimator = mParentView.getItemAnimator(); 273 itemAnimator.isRunning(new RecyclerView.ItemAnimator.ItemAnimatorFinishedListener() { 274 @Override 275 public void onAnimationsFinished() { 276 if (mLocalesToSetNext == null || mLocalesToSetNext.equals(mLocalesSetLast)) { 277 // All animations finished, but the locale list did not change 278 return; 279 } 280 281 LocalePicker.updateLocales(mLocalesToSetNext); 282 mLocalesSetLast = mLocalesToSetNext; 283 mLocalesToSetNext = null; 284 285 mNumberFormatter = NumberFormat.getNumberInstance(Locale.getDefault()); 286 } 287 }); 288 } 289 290 private void setDragEnabled(boolean enabled) { 291 mDragEnabled = enabled; 292 } 293 294 /** 295 * Saves the list of checked locales to preserve status when the list is destroyed. 296 * (for instance when the device is rotated) 297 * @param outInstanceState Bundle in which to place the saved state 298 */ 299 public void saveState(Bundle outInstanceState) { 300 if (outInstanceState != null) { 301 final ArrayList<String> selectedLocales = new ArrayList<>(); 302 for (LocaleStore.LocaleInfo li : mFeedItemList) { 303 if (li.getChecked()) { 304 selectedLocales.add(li.getId()); 305 } 306 } 307 outInstanceState.putStringArrayList(CFGKEY_SELECTED_LOCALES, selectedLocales); 308 } 309 } 310 311 /** 312 * Restores the list of checked locales to preserve status when the list is recreated. 313 * (for instance when the device is rotated) 314 * @param savedInstanceState Bundle with the data saved by {@link #saveState(Bundle)} 315 */ 316 public void restoreState(Bundle savedInstanceState) { 317 if (savedInstanceState != null && mRemoveMode) { 318 final ArrayList<String> selectedLocales = 319 savedInstanceState.getStringArrayList(CFGKEY_SELECTED_LOCALES); 320 if (selectedLocales == null || selectedLocales.isEmpty()) { 321 return; 322 } 323 for (LocaleStore.LocaleInfo li : mFeedItemList) { 324 li.setChecked(selectedLocales.contains(li.getId())); 325 } 326 notifyItemRangeChanged(0, mFeedItemList.size()); 327 } 328 } 329} 330