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