forked from SimplicityApks/ReminderDatePicker
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathPickerSpinner.java
293 lines (268 loc) · 12.8 KB
/
PickerSpinner.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
package com.simplicityapks.reminderdatepicker.lib;
import android.content.Context;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Spinner;
import android.widget.SpinnerAdapter;
import java.util.ArrayList;
import java.util.List;
/**
* Base class for both DateSpinner and TimeSpinner.
* This is a Spinner with the following additional, optional features:
*
* 1. A custom last list item (footer), which won't get selected on click. Instead, onFooterClick() will be called.
* 2. Items with secondary text, due to integration with {@link com.simplicityapks.reminderdatepicker.lib.PickerSpinnerAdapter}
* 3. Select items which are not currently in the spinner items (use {@link #selectTemporary(TwinTextItem)}.
* 4. Dynamic and easy modifying the spinner items without having to worry about selection changes (use the ...AdapterItem...() methods)
*/
public abstract class PickerSpinner extends Spinner {
// Indicates that the onItemSelectedListener callback should not be passed to the listener.
private final ArrayList<Integer> interceptSelectionCallbacks = new ArrayList<>();
// Indicates that the selection should be restored after initialization (setSelection has not been called externally)
private boolean restoreTemporarySelection = true;
// Indicates that the temporary item should be reselected after an item is removed
private boolean reselectTemporaryItem = false;
/**
* Construct a new PickerSpinner with the given context's theme.
* @param context The Context the view is running in, through which it can access the current theme, resources, etc.
*/
public PickerSpinner(Context context) {
this(context, null);
}
/**
* Construct a new PickerSpinner with the given context's theme and the supplied attribute set.
* @param context The Context the view is running in, through which it can access the current theme, resources, etc.
* @param attrs The attributes of the XML tag that is inflating the view.
*/
public PickerSpinner(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
/**
* Construct a new PickerSpinner with the given context's theme, the supplied attribute set, and default style.
* @param context The Context the view is running in, through which it can access the current theme, resources, etc.
* @param attrs The attributes of the XML tag that is inflating the view.
* @param defStyle The default style to apply to this view. If 0, no style will be applied (beyond
* what is included in the theme). This may either be an attribute resource, whose
* value will be retrieved from the current theme, or an explicit style resource.
*/
public PickerSpinner(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs);
initAdapter(context);
}
protected void initAdapter(Context context) {
// create our simple adapter with default layouts and set it here:
PickerSpinnerAdapter adapter = new PickerSpinnerAdapter(context, getSpinnerItems(),
new TwinTextItem.Simple(getFooter(), null));
setAdapter(adapter);
}
@NonNull
@Override
public Parcelable onSaveInstanceState() {
// our temporary selection will not be saved
if(getSelectedItemPosition() == getCount()) {
Bundle state = new Bundle();
state.putParcelable("superState", super.onSaveInstanceState());
// save the TwinTextItem using its toString() method
state.putString("temporaryItem", getSelectedItem().toString());
return state;
}
else return super.onSaveInstanceState();
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if(state instanceof Bundle) {
Bundle bundle = (Bundle) state;
super.onRestoreInstanceState(bundle.getParcelable("superState"));
// restore the temporary selection, we need to wait till the end of the message queue
final String tempItem = bundle.getString("temporaryItem");
restoreTemporarySelection = true;
post(new Runnable() {
@Override
public void run() {
if(restoreTemporarySelection)
restoreTemporarySelection(tempItem);
}
});
}
else super.onRestoreInstanceState(state);
}
/**
* Sets the Adapter used to provide the data which backs this Spinner. Needs to be an {@link com.simplicityapks.reminderdatepicker.lib.PickerSpinnerAdapter}
* to be used with this class. Note that a PickerSpinner automatically creates its own adapter
* so you should not need to call this method.
* @param adapter The PickerSpinnerAdapter to be used.
* @throws IllegalArgumentException If adapter is not a PickerSpinnerAdapter.
*/
@Override
public void setAdapter(SpinnerAdapter adapter) {
if(adapter instanceof PickerSpinnerAdapter)
super.setAdapter(adapter);
else throw new IllegalArgumentException(
"adapter must extend PickerSpinnerAdapter to be used with this class");
}
/**
* {@inheritDoc}
*/
@Override
public void setSelection(int position) {
if(position == getCount()-1 && ((PickerSpinnerAdapter)getAdapter()).hasFooter())
onFooterClick(); // the footer has been clicked, so don't update the selection
else {
// remove any previous temporary selection:
((PickerSpinnerAdapter)getAdapter()).selectTemporary(null);
// check that the selection goes through:
interceptSelectionCallbacks.clear();
super.setSelection(position);
super.setSelection(position, false);
}
}
/**
* Equivalent to {@link #setSelection(int)}, but without calling any onItemSelectedListeners or
* checking for footer clicks.
*/
private void setSelectionQuietly(int position) {
// intercept the callback here:
interceptSelectionCallbacks.add(position);
super.setSelection(position, false); // No idea why both setSelections are needed but it only works with both
super.setSelection(position);
}
/**
* Push an item to be selected, but not shown in the dropdown menu. This is similar to calling
* setText(item.toString()) if a Spinner had such a method.
* @param item The item to select, or null to remove any temporary selection.
*/
public void selectTemporary(TwinTextItem item) {
restoreTemporarySelection = false;
// if we just want to clear the selection:
if(item == null) {
setSelection(getLastItemPosition());
// the call is passed on to the adapter in setSelection.
return;
}
// pass on the call to the adapter:
((PickerSpinnerAdapter)getAdapter()).selectTemporary(item);
final int tempItemPosition = getCount();
if(getSelectedItemPosition() == tempItemPosition) {
// this is quite a hack, first reset the position to 0 but intercept the callback,
// then redo the selection:
setSelectionQuietly(0);
}
super.setSelection(tempItemPosition);
}
@Override
public void setOnItemSelectedListener(final OnItemSelectedListener listener) {
super.setOnItemSelectedListener(
new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if(interceptSelectionCallbacks.contains(position)) {
interceptSelectionCallbacks.remove((Integer) position);
if(reselectTemporaryItem) {
if (position != getAdapter().getCount())
setSelectionQuietly(getAdapter().getCount());
reselectTemporaryItem = false;
}
}
// sometimes during rotation or initialization onItemSelected will be called with the footer selected, catch that
else if(!(((PickerSpinnerAdapter) getAdapter()).hasFooter()
&& position == getLastItemPosition() + 1))
listener.onItemSelected(parent, view, position, id);
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
listener.onNothingSelected(parent);
}
}
);
}
/**
* Gets the position of the last item in the dataset, after which the footer and temporary selection have their index.
* @return The last selectable position.
*/
public int getLastItemPosition() {
return getCount() - (((PickerSpinnerAdapter) getAdapter()).hasFooter()?2:1);
}
/**
* Adds the item to the adapter's data set and takes care of handling selection changes.
* Always call this method instead of getAdapter().add().
* @param item The item to insert.
*/
public void addAdapterItem(TwinTextItem item) {
insertAdapterItem(item, getLastItemPosition()+1);
}
/**
* Inserts the item at the specified index into the adapter's data set and takes care of handling selection changes.
* Always call this method instead of getAdapter().insert().
* @param item The item to insert.
* @param index The index where it'll be at.
*/
public void insertAdapterItem(TwinTextItem item, int index) {
int selection = getSelectedItemPosition();
((PickerSpinnerAdapter)getAdapter()).insert(item, index);
if(index <= selection)
setSelectionQuietly(selection+1);
}
/**
* Removes the specified item from the adapter and takes care of handling selection changes.
* Always call this method instead of getAdapter().remove().
* @param index The index of the item to be removed.
*/
public void removeAdapterItemAt(int index) {
PickerSpinnerAdapter adapter = (PickerSpinnerAdapter) getAdapter();
int count = adapter.getCount();
int selection = getSelectedItemPosition();
// check which item will be removed:
if(index == count) // temporary selection
selectTemporary(null);
else if (index == count-1 && adapter.hasFooter()) { // footer
if(selection == count)
setSelectionQuietly(selection - 1);
adapter.setFooter(null);
} else { // a normal item
// keep the right selection in either of these cases:
if(index == selection) {// we delete the selected item and
if(index == getLastItemPosition()) // it is the last real item
setSelection(selection - 1);
else {
// we need to reselect the current item:
setSelectionQuietly(index==0 && count>1? 1 : 0);
setSelection(selection);
}
}
else if(index < selection && selection!=count) // we remove an item above it
setSelectionQuietly(selection - 1);
adapter.remove(adapter.getItem(index));
if(selection == count) { // we have a temporary item selected
reselectTemporaryItem = true;
setSelectionQuietly(selection - 1);
}
}
}
/**
* Gets the default list of items to be inflated into the Spinner, will be called once on
* initializing the Spinner. Should use lazy initialization in inherited classes.
* @return The List of Objects whose toString() method will be called for the items, or null.
*/
public abstract List<TwinTextItem> getSpinnerItems();
/**
* Gets the CharSequence to be shown as footer in the drop down menu.
* @return The footer, or null to disable showing it.
*/
public abstract CharSequence getFooter();
/**
* Built-in listener for clicks on the footer. Note that the footer will not replace the
* selection and you still need a separate OnItemSelectedListener.
*/
public abstract void onFooterClick();
/**
* Called to restore a previously saved temporary selection. The given codeString has been saved
* using the toString() method on the TwinTextItem. This method should ideally only call
* {@link #selectTemporary(TwinTextItem)} with a new TwinTextItem parsed from the codeString.
* @param codeString The raw String saved from the item's toString() method.
*/
protected abstract void restoreTemporarySelection(String codeString);
}