GuidedActionAdapter.java revision a51a405279fb81135abbb7c25ba431842582c8c8
1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License
10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 * or implied. See the License for the specific language governing permissions and limitations under
12 * the License.
13 */
14package android.support.v17.leanback.widget;
15
16import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
17
18import android.support.annotation.RestrictTo;
19import android.support.v7.widget.RecyclerView;
20import android.support.v7.widget.RecyclerView.ViewHolder;
21import android.util.Log;
22import android.view.KeyEvent;
23import android.view.View;
24import android.view.ViewGroup;
25import android.view.ViewParent;
26import android.view.inputmethod.EditorInfo;
27import android.widget.EditText;
28import android.widget.TextView;
29import android.widget.TextView.OnEditorActionListener;
30
31import java.util.ArrayList;
32import java.util.List;
33
34/**
35 * GuidedActionAdapter instantiates views for guided actions, and manages their interactions.
36 * Presentation (view creation and state animation) is delegated to a {@link
37 * GuidedActionsStylist}, while clients are notified of interactions via
38 * {@link GuidedActionAdapter.ClickListener} and {@link GuidedActionAdapter.FocusListener}.
39 * @hide
40 */
41@RestrictTo(LIBRARY_GROUP)
42public class GuidedActionAdapter extends RecyclerView.Adapter {
43    static final String TAG = "GuidedActionAdapter";
44    static final boolean DEBUG = false;
45
46    static final String TAG_EDIT = "EditableAction";
47    static final boolean DEBUG_EDIT = false;
48
49    /**
50     * Object listening for click events within a {@link GuidedActionAdapter}.
51     */
52    public interface ClickListener {
53
54        /**
55         * Called when the user clicks on an action.
56         */
57        public void onGuidedActionClicked(GuidedAction action);
58
59    }
60
61    /**
62     * Object listening for focus events within a {@link GuidedActionAdapter}.
63     */
64    public interface FocusListener {
65
66        /**
67         * Called when the user focuses on an action.
68         */
69        public void onGuidedActionFocused(GuidedAction action);
70    }
71
72    /**
73     * Object listening for edit events within a {@link GuidedActionAdapter}.
74     */
75    public interface EditListener {
76
77        /**
78         * Called when the user exits edit mode on an action.
79         */
80        public void onGuidedActionEditCanceled(GuidedAction action);
81
82        /**
83         * Called when the user exits edit mode on an action and process confirm button in IME.
84         */
85        public long onGuidedActionEditedAndProceed(GuidedAction action);
86
87        /**
88         * Called when Ime Open
89         */
90        public void onImeOpen();
91
92        /**
93         * Called when Ime Close
94         */
95        public void onImeClose();
96    }
97
98    private final boolean mIsSubAdapter;
99    private final ActionOnKeyListener mActionOnKeyListener;
100    private final ActionOnFocusListener mActionOnFocusListener;
101    private final ActionEditListener mActionEditListener;
102    private final List<GuidedAction> mActions;
103    private ClickListener mClickListener;
104    final GuidedActionsStylist mStylist;
105    private final View.OnClickListener mOnClickListener = new View.OnClickListener() {
106        @Override
107        public void onClick(View v) {
108            if (v != null && v.getWindowToken() != null && getRecyclerView() != null) {
109                GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)
110                        getRecyclerView().getChildViewHolder(v);
111                GuidedAction action = avh.getAction();
112                if (action.hasTextEditable()) {
113                    if (DEBUG_EDIT) Log.v(TAG_EDIT, "openIme by click");
114                    mGroup.openIme(GuidedActionAdapter.this, avh);
115                } else if (action.hasEditableActivatorView()) {
116                    if (DEBUG_EDIT) Log.v(TAG_EDIT, "toggle editing mode by click");
117                    performOnActionClick(avh);
118                } else {
119                    handleCheckedActions(avh);
120                    if (action.isEnabled() && !action.infoOnly()) {
121                        performOnActionClick(avh);
122                    }
123                }
124            }
125        }
126    };
127    GuidedActionAdapterGroup mGroup;
128
129    /**
130     * Constructs a GuidedActionAdapter with the given list of guided actions, the given click and
131     * focus listeners, and the given presenter.
132     * @param actions The list of guided actions this adapter will manage.
133     * @param focusListener The focus listener for items in this adapter.
134     * @param presenter The presenter that will manage the display of items in this adapter.
135     */
136    public GuidedActionAdapter(List<GuidedAction> actions, ClickListener clickListener,
137            FocusListener focusListener, GuidedActionsStylist presenter, boolean isSubAdapter) {
138        super();
139        mActions = actions == null ? new ArrayList<GuidedAction>() :
140                new ArrayList<GuidedAction>(actions);
141        mClickListener = clickListener;
142        mStylist = presenter;
143        mActionOnKeyListener = new ActionOnKeyListener();
144        mActionOnFocusListener = new ActionOnFocusListener(focusListener);
145        mActionEditListener = new ActionEditListener();
146        mIsSubAdapter = isSubAdapter;
147    }
148
149    /**
150     * Sets the list of actions managed by this adapter.
151     * @param actions The list of actions to be managed.
152     */
153    public void setActions(List<GuidedAction> actions) {
154        mStylist.collapseAction(false);
155        mActionOnFocusListener.unFocus();
156        mActions.clear();
157        mActions.addAll(actions);
158        notifyDataSetChanged();
159    }
160
161    /**
162     * Returns the count of actions managed by this adapter.
163     * @return The count of actions managed by this adapter.
164     */
165    public int getCount() {
166        return mActions.size();
167    }
168
169    /**
170     * Returns the GuidedAction at the given position in the managed list.
171     * @param position The position of the desired GuidedAction.
172     * @return The GuidedAction at the given position.
173     */
174    public GuidedAction getItem(int position) {
175        return mActions.get(position);
176    }
177
178    /**
179     * Return index of action in array
180     * @param action Action to search index.
181     * @return Index of Action in array.
182     */
183    public int indexOf(GuidedAction action) {
184        return mActions.indexOf(action);
185    }
186
187    /**
188     * @return GuidedActionsStylist used to build the actions list UI.
189     */
190    public GuidedActionsStylist getGuidedActionsStylist() {
191        return mStylist;
192    }
193
194    /**
195     * Sets the click listener for items managed by this adapter.
196     * @param clickListener The click listener for this adapter.
197     */
198    public void setClickListener(ClickListener clickListener) {
199        mClickListener = clickListener;
200    }
201
202    /**
203     * Sets the focus listener for items managed by this adapter.
204     * @param focusListener The focus listener for this adapter.
205     */
206    public void setFocusListener(FocusListener focusListener) {
207        mActionOnFocusListener.setFocusListener(focusListener);
208    }
209
210    /**
211     * Used for serialization only.
212     * @hide
213     */
214    @RestrictTo(LIBRARY_GROUP)
215    public List<GuidedAction> getActions() {
216        return new ArrayList<GuidedAction>(mActions);
217    }
218
219    /**
220     * {@inheritDoc}
221     */
222    @Override
223    public int getItemViewType(int position) {
224        return mStylist.getItemViewType(mActions.get(position));
225    }
226
227    RecyclerView getRecyclerView() {
228        return mIsSubAdapter ? mStylist.getSubActionsGridView() : mStylist.getActionsGridView();
229    }
230
231    /**
232     * {@inheritDoc}
233     */
234    @Override
235    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
236        GuidedActionsStylist.ViewHolder vh = mStylist.onCreateViewHolder(parent, viewType);
237        View v = vh.itemView;
238        v.setOnKeyListener(mActionOnKeyListener);
239        v.setOnClickListener(mOnClickListener);
240        v.setOnFocusChangeListener(mActionOnFocusListener);
241
242        setupListeners(vh.getEditableTitleView());
243        setupListeners(vh.getEditableDescriptionView());
244
245        return vh;
246    }
247
248    private void setupListeners(EditText edit) {
249        if (edit != null) {
250            edit.setPrivateImeOptions("EscapeNorth=1;");
251            edit.setOnEditorActionListener(mActionEditListener);
252            if (edit instanceof ImeKeyMonitor) {
253                ImeKeyMonitor monitor = (ImeKeyMonitor)edit;
254                monitor.setImeKeyListener(mActionEditListener);
255            }
256        }
257    }
258
259    /**
260     * {@inheritDoc}
261     */
262    @Override
263    public void onBindViewHolder(ViewHolder holder, int position) {
264        if (position >= mActions.size()) {
265            return;
266        }
267        final GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)holder;
268        GuidedAction action = mActions.get(position);
269        mStylist.onBindViewHolder(avh, action);
270    }
271
272    /**
273     * {@inheritDoc}
274     */
275    @Override
276    public int getItemCount() {
277        return mActions.size();
278    }
279
280    private class ActionOnFocusListener implements View.OnFocusChangeListener {
281
282        private FocusListener mFocusListener;
283        private View mSelectedView;
284
285        ActionOnFocusListener(FocusListener focusListener) {
286            mFocusListener = focusListener;
287        }
288
289        public void setFocusListener(FocusListener focusListener) {
290            mFocusListener = focusListener;
291        }
292
293        public void unFocus() {
294            if (mSelectedView != null && getRecyclerView() != null) {
295                ViewHolder vh = getRecyclerView().getChildViewHolder(mSelectedView);
296                if (vh != null) {
297                    GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)vh;
298                    mStylist.onAnimateItemFocused(avh, false);
299                } else {
300                    Log.w(TAG, "RecyclerView returned null view holder",
301                            new Throwable());
302                }
303            }
304        }
305
306        @Override
307        public void onFocusChange(View v, boolean hasFocus) {
308            if (getRecyclerView() == null) {
309                return;
310            }
311            GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)
312                    getRecyclerView().getChildViewHolder(v);
313            if (hasFocus) {
314                mSelectedView = v;
315                if (mFocusListener != null) {
316                    // We still call onGuidedActionFocused so that listeners can clear
317                    // state if they want.
318                    mFocusListener.onGuidedActionFocused(avh.getAction());
319                }
320            } else {
321                if (mSelectedView == v) {
322                    mStylist.onAnimateItemPressedCancelled(avh);
323                    mSelectedView = null;
324                }
325            }
326            mStylist.onAnimateItemFocused(avh, hasFocus);
327        }
328    }
329
330    public GuidedActionsStylist.ViewHolder findSubChildViewHolder(View v) {
331        // Needed because RecyclerView.getChildViewHolder does not traverse the hierarchy
332        if (getRecyclerView() == null) {
333            return null;
334        }
335        GuidedActionsStylist.ViewHolder result = null;
336        ViewParent parent = v.getParent();
337        while (parent != getRecyclerView() && parent != null && v != null) {
338            v = (View)parent;
339            parent = parent.getParent();
340        }
341        if (parent != null && v != null) {
342            result = (GuidedActionsStylist.ViewHolder)getRecyclerView().getChildViewHolder(v);
343        }
344        return result;
345    }
346
347    public void handleCheckedActions(GuidedActionsStylist.ViewHolder avh) {
348        GuidedAction action = avh.getAction();
349        int actionCheckSetId = action.getCheckSetId();
350        if (getRecyclerView() != null && actionCheckSetId != GuidedAction.NO_CHECK_SET) {
351            // Find any actions that are checked and are in the same group
352            // as the selected action. Fade their checkmarks out.
353            if (actionCheckSetId != GuidedAction.CHECKBOX_CHECK_SET_ID) {
354                for (int i = 0, size = mActions.size(); i < size; i++) {
355                    GuidedAction a = mActions.get(i);
356                    if (a != action && a.getCheckSetId() == actionCheckSetId && a.isChecked()) {
357                        a.setChecked(false);
358                        GuidedActionsStylist.ViewHolder vh = (GuidedActionsStylist.ViewHolder)
359                                getRecyclerView().findViewHolderForPosition(i);
360                        if (vh != null) {
361                            mStylist.onAnimateItemChecked(vh, false);
362                        }
363                    }
364                }
365            }
366
367            // If we we'ren't already checked, fade our checkmark in.
368            if (!action.isChecked()) {
369                action.setChecked(true);
370                mStylist.onAnimateItemChecked(avh, true);
371            } else {
372                if (actionCheckSetId == GuidedAction.CHECKBOX_CHECK_SET_ID) {
373                    action.setChecked(false);
374                    mStylist.onAnimateItemChecked(avh, false);
375                }
376            }
377        }
378    }
379
380    public void performOnActionClick(GuidedActionsStylist.ViewHolder avh) {
381        if (mClickListener != null) {
382            mClickListener.onGuidedActionClicked(avh.getAction());
383        }
384    }
385
386    private class ActionOnKeyListener implements View.OnKeyListener {
387
388        private boolean mKeyPressed = false;
389
390        ActionOnKeyListener() {
391        }
392
393        /**
394         * Now only handles KEYCODE_ENTER and KEYCODE_NUMPAD_ENTER key event.
395         */
396        @Override
397        public boolean onKey(View v, int keyCode, KeyEvent event) {
398            if (v == null || event == null || getRecyclerView() == null) {
399                return false;
400            }
401            boolean handled = false;
402            switch (keyCode) {
403                case KeyEvent.KEYCODE_DPAD_CENTER:
404                case KeyEvent.KEYCODE_NUMPAD_ENTER:
405                case KeyEvent.KEYCODE_BUTTON_X:
406                case KeyEvent.KEYCODE_BUTTON_Y:
407                case KeyEvent.KEYCODE_ENTER:
408
409                    GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)
410                            getRecyclerView().getChildViewHolder(v);
411                    GuidedAction action = avh.getAction();
412
413                    if (!action.isEnabled() || action.infoOnly()) {
414                        if (event.getAction() == KeyEvent.ACTION_DOWN) {
415                            // TODO: requires API 19
416                            //playSound(v, AudioManager.FX_KEYPRESS_INVALID);
417                        }
418                        return true;
419                    }
420
421                    switch (event.getAction()) {
422                        case KeyEvent.ACTION_DOWN:
423                            if (DEBUG) {
424                                Log.d(TAG, "Enter Key down");
425                            }
426                            if (!mKeyPressed) {
427                                mKeyPressed = true;
428                                mStylist.onAnimateItemPressed(avh, mKeyPressed);
429                            }
430                            break;
431                        case KeyEvent.ACTION_UP:
432                            if (DEBUG) {
433                                Log.d(TAG, "Enter Key up");
434                            }
435                            // Sometimes we are losing ACTION_DOWN for the first ENTER after pressed
436                            // Escape in IME.
437                            if (mKeyPressed) {
438                                mKeyPressed = false;
439                                mStylist.onAnimateItemPressed(avh, mKeyPressed);
440                            }
441                            break;
442                        default:
443                            break;
444                    }
445                    break;
446                default:
447                    break;
448            }
449            return handled;
450        }
451
452    }
453
454    private class ActionEditListener implements OnEditorActionListener,
455            ImeKeyMonitor.ImeKeyListener {
456
457        ActionEditListener() {
458        }
459
460        @Override
461        public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
462            if (DEBUG_EDIT) Log.v(TAG_EDIT, "IME action: " + actionId);
463            boolean handled = false;
464            if (actionId == EditorInfo.IME_ACTION_NEXT
465                    || actionId == EditorInfo.IME_ACTION_DONE) {
466                mGroup.fillAndGoNext(GuidedActionAdapter.this, v);
467                handled = true;
468            } else if (actionId == EditorInfo.IME_ACTION_NONE) {
469                if (DEBUG_EDIT) Log.v(TAG_EDIT, "closeIme escape north");
470                // Escape north handling: stay on current item, but close editor
471                handled = true;
472                mGroup.fillAndStay(GuidedActionAdapter.this, v);
473            }
474            return handled;
475        }
476
477        @Override
478        public boolean onKeyPreIme(EditText editText, int keyCode, KeyEvent event) {
479            if (DEBUG_EDIT) Log.v(TAG_EDIT, "IME key: " + keyCode);
480            if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
481                mGroup.fillAndStay(GuidedActionAdapter.this, editText);
482                return true;
483            } else if (keyCode == KeyEvent.KEYCODE_ENTER
484                    && event.getAction() == KeyEvent.ACTION_UP) {
485                mGroup.fillAndGoNext(GuidedActionAdapter.this, editText);
486                return true;
487            }
488            return false;
489        }
490
491    }
492
493}
494