GuidedActionAdapter.java revision b88b36aa081a500eb0e9d4be0bac85b33cd57dde
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.hasTextEditable()) {
112                    if (DEBUG_EDIT) Log.v(TAG_EDIT, "openIme by click");
113                    mGroup.openIme(GuidedActionAdapter.this, avh);
114                } else if (action.hasEditableActivatorView()) {
115                    if (DEBUG_EDIT) Log.v(TAG_EDIT, "toggle editing mode by click");
116                    getGuidedActionsStylist().setEditingMode(avh, avh.getAction(),
117                            !avh.isInEditingActivatorView());
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        mActionOnFocusListener.unFocus();
155        mActions.clear();
156        mActions.addAll(actions);
157        notifyDataSetChanged();
158    }
159
160    /**
161     * Returns the count of actions managed by this adapter.
162     * @return The count of actions managed by this adapter.
163     */
164    public int getCount() {
165        return mActions.size();
166    }
167
168    /**
169     * Returns the GuidedAction at the given position in the managed list.
170     * @param position The position of the desired GuidedAction.
171     * @return The GuidedAction at the given position.
172     */
173    public GuidedAction getItem(int position) {
174        return mActions.get(position);
175    }
176
177    /**
178     * Return index of action in array
179     * @param action Action to search index.
180     * @return Index of Action in array.
181     */
182    public int indexOf(GuidedAction action) {
183        return mActions.indexOf(action);
184    }
185
186    /**
187     * @return GuidedActionsStylist used to build the actions list UI.
188     */
189    public GuidedActionsStylist getGuidedActionsStylist() {
190        return mStylist;
191    }
192
193    /**
194     * Sets the click listener for items managed by this adapter.
195     * @param clickListener The click listener for this adapter.
196     */
197    public void setClickListener(ClickListener clickListener) {
198        mClickListener = clickListener;
199    }
200
201    /**
202     * Sets the focus listener for items managed by this adapter.
203     * @param focusListener The focus listener for this adapter.
204     */
205    public void setFocusListener(FocusListener focusListener) {
206        mActionOnFocusListener.setFocusListener(focusListener);
207    }
208
209    /**
210     * Used for serialization only.
211     * @hide
212     */
213    public List<GuidedAction> getActions() {
214        return new ArrayList<GuidedAction>(mActions);
215    }
216
217    /**
218     * {@inheritDoc}
219     */
220    @Override
221    public int getItemViewType(int position) {
222        return mStylist.getItemViewType(mActions.get(position));
223    }
224
225    private RecyclerView getRecyclerView() {
226        return mIsSubAdapter ? mStylist.getSubActionsGridView() : mStylist.getActionsGridView();
227    }
228
229    /**
230     * {@inheritDoc}
231     */
232    @Override
233    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
234        GuidedActionsStylist.ViewHolder vh = mStylist.onCreateViewHolder(parent, viewType);
235        View v = vh.itemView;
236        v.setOnKeyListener(mActionOnKeyListener);
237        v.setOnClickListener(mOnClickListener);
238        v.setOnFocusChangeListener(mActionOnFocusListener);
239
240        setupListeners(vh.getEditableTitleView());
241        setupListeners(vh.getEditableDescriptionView());
242
243        return vh;
244    }
245
246    private void setupListeners(EditText edit) {
247        if (edit != null) {
248            edit.setPrivateImeOptions("EscapeNorth=1;");
249            edit.setOnEditorActionListener(mActionEditListener);
250            if (edit instanceof ImeKeyMonitor) {
251                ImeKeyMonitor monitor = (ImeKeyMonitor)edit;
252                monitor.setImeKeyListener(mActionEditListener);
253            }
254        }
255    }
256
257    /**
258     * {@inheritDoc}
259     */
260    @Override
261    public void onBindViewHolder(ViewHolder holder, int position) {
262        if (position >= mActions.size()) {
263            return;
264        }
265        final GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)holder;
266        GuidedAction action = mActions.get(position);
267        mStylist.onBindViewHolder(avh, action);
268    }
269
270    /**
271     * {@inheritDoc}
272     */
273    @Override
274    public int getItemCount() {
275        return mActions.size();
276    }
277
278    private class ActionOnFocusListener implements View.OnFocusChangeListener {
279
280        private FocusListener mFocusListener;
281        private View mSelectedView;
282
283        ActionOnFocusListener(FocusListener focusListener) {
284            mFocusListener = focusListener;
285        }
286
287        public void setFocusListener(FocusListener focusListener) {
288            mFocusListener = focusListener;
289        }
290
291        public void unFocus() {
292            if (mSelectedView != null && getRecyclerView() != null) {
293                ViewHolder vh = getRecyclerView().getChildViewHolder(mSelectedView);
294                if (vh != null) {
295                    GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)vh;
296                    mStylist.onAnimateItemFocused(avh, false);
297                } else {
298                    Log.w(TAG, "RecyclerView returned null view holder",
299                            new Throwable());
300                }
301            }
302        }
303
304        @Override
305        public void onFocusChange(View v, boolean hasFocus) {
306            if (getRecyclerView() == null) {
307                return;
308            }
309            GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)
310                    getRecyclerView().getChildViewHolder(v);
311            if (hasFocus) {
312                mSelectedView = v;
313                if (mFocusListener != null) {
314                    // We still call onGuidedActionFocused so that listeners can clear
315                    // state if they want.
316                    mFocusListener.onGuidedActionFocused(avh.getAction());
317                }
318            } else {
319                if (mSelectedView == v) {
320                    mStylist.onAnimateItemPressedCancelled(avh);
321                    mSelectedView = null;
322                }
323            }
324            mStylist.onAnimateItemFocused(avh, hasFocus);
325        }
326    }
327
328    public GuidedActionsStylist.ViewHolder findSubChildViewHolder(View v) {
329        // Needed because RecyclerView.getChildViewHolder does not traverse the hierarchy
330        if (getRecyclerView() == null) {
331            return null;
332        }
333        GuidedActionsStylist.ViewHolder result = null;
334        ViewParent parent = v.getParent();
335        while (parent != getRecyclerView() && parent != null && v != null) {
336            v = (View)parent;
337            parent = parent.getParent();
338        }
339        if (parent != null && v != null) {
340            result = (GuidedActionsStylist.ViewHolder)getRecyclerView().getChildViewHolder(v);
341        }
342        return result;
343    }
344
345    public void handleCheckedActions(GuidedActionsStylist.ViewHolder avh) {
346        GuidedAction action = avh.getAction();
347        int actionCheckSetId = action.getCheckSetId();
348        if (getRecyclerView() != null && actionCheckSetId != GuidedAction.NO_CHECK_SET) {
349            // Find any actions that are checked and are in the same group
350            // as the selected action. Fade their checkmarks out.
351            if (actionCheckSetId != GuidedAction.CHECKBOX_CHECK_SET_ID) {
352                for (int i = 0, size = mActions.size(); i < size; i++) {
353                    GuidedAction a = mActions.get(i);
354                    if (a != action && a.getCheckSetId() == actionCheckSetId && a.isChecked()) {
355                        a.setChecked(false);
356                        GuidedActionsStylist.ViewHolder vh = (GuidedActionsStylist.ViewHolder)
357                                getRecyclerView().findViewHolderForPosition(i);
358                        if (vh != null) {
359                            mStylist.onAnimateItemChecked(vh, false);
360                        }
361                    }
362                }
363            }
364
365            // If we we'ren't already checked, fade our checkmark in.
366            if (!action.isChecked()) {
367                action.setChecked(true);
368                mStylist.onAnimateItemChecked(avh, true);
369            } else {
370                if (actionCheckSetId == GuidedAction.CHECKBOX_CHECK_SET_ID) {
371                    action.setChecked(false);
372                    mStylist.onAnimateItemChecked(avh, false);
373                }
374            }
375        }
376    }
377
378    public void performOnActionClick(GuidedActionsStylist.ViewHolder avh) {
379        if (mClickListener != null) {
380            mClickListener.onGuidedActionClicked(avh.getAction());
381        }
382    }
383
384    private class ActionOnKeyListener implements View.OnKeyListener {
385
386        private boolean mKeyPressed = false;
387
388        /**
389         * Now only handles KEYCODE_ENTER and KEYCODE_NUMPAD_ENTER key event.
390         */
391        @Override
392        public boolean onKey(View v, int keyCode, KeyEvent event) {
393            if (v == null || event == null || getRecyclerView() == null) {
394                return false;
395            }
396            boolean handled = false;
397            switch (keyCode) {
398                case KeyEvent.KEYCODE_DPAD_CENTER:
399                case KeyEvent.KEYCODE_NUMPAD_ENTER:
400                case KeyEvent.KEYCODE_BUTTON_X:
401                case KeyEvent.KEYCODE_BUTTON_Y:
402                case KeyEvent.KEYCODE_ENTER:
403
404                    GuidedActionsStylist.ViewHolder avh = (GuidedActionsStylist.ViewHolder)
405                            getRecyclerView().getChildViewHolder(v);
406                    GuidedAction action = avh.getAction();
407
408                    if (!action.isEnabled() || action.infoOnly()) {
409                        if (event.getAction() == KeyEvent.ACTION_DOWN) {
410                            // TODO: requires API 19
411                            //playSound(v, AudioManager.FX_KEYPRESS_INVALID);
412                        }
413                        return true;
414                    }
415
416                    switch (event.getAction()) {
417                        case KeyEvent.ACTION_DOWN:
418                            if (DEBUG) {
419                                Log.d(TAG, "Enter Key down");
420                            }
421                            if (!mKeyPressed) {
422                                mKeyPressed = true;
423                                mStylist.onAnimateItemPressed(avh, mKeyPressed);
424                            }
425                            break;
426                        case KeyEvent.ACTION_UP:
427                            if (DEBUG) {
428                                Log.d(TAG, "Enter Key up");
429                            }
430                            // Sometimes we are losing ACTION_DOWN for the first ENTER after pressed
431                            // Escape in IME.
432                            if (mKeyPressed) {
433                                mKeyPressed = false;
434                                mStylist.onAnimateItemPressed(avh, mKeyPressed);
435                            }
436                            break;
437                        default:
438                            break;
439                    }
440                    break;
441                default:
442                    break;
443            }
444            return handled;
445        }
446
447    }
448
449    private class ActionEditListener implements OnEditorActionListener,
450            ImeKeyMonitor.ImeKeyListener {
451
452        @Override
453        public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
454            if (DEBUG_EDIT) Log.v(TAG_EDIT, "IME action: " + actionId);
455            boolean handled = false;
456            if (actionId == EditorInfo.IME_ACTION_NEXT ||
457                actionId == EditorInfo.IME_ACTION_DONE) {
458                mGroup.fillAndGoNext(GuidedActionAdapter.this, v);
459                handled = true;
460            } else if (actionId == EditorInfo.IME_ACTION_NONE) {
461                if (DEBUG_EDIT) Log.v(TAG_EDIT, "closeIme escape north");
462                // Escape north handling: stay on current item, but close editor
463                handled = true;
464                mGroup.fillAndStay(GuidedActionAdapter.this, v);
465            }
466            return handled;
467        }
468
469        @Override
470        public boolean onKeyPreIme(EditText editText, int keyCode, KeyEvent event) {
471            if (DEBUG_EDIT) Log.v(TAG_EDIT, "IME key: " + keyCode);
472            if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
473                mGroup.fillAndStay(GuidedActionAdapter.this, editText);
474            } else if (keyCode == KeyEvent.KEYCODE_ENTER && event.getAction() ==
475                    KeyEvent.ACTION_UP) {
476                mGroup.fillAndGoNext(GuidedActionAdapter.this, editText);
477            }
478            return false;
479        }
480
481    }
482
483}
484