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