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