Home | History | Annotate | Download | only in old
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.tv.settings.dialog.old;
     18 
     19 import android.animation.Animator;
     20 import android.animation.AnimatorListenerAdapter;
     21 import android.animation.ObjectAnimator;
     22 import android.content.Context;
     23 import android.content.res.Resources;
     24 import android.graphics.Bitmap;
     25 import android.graphics.drawable.Drawable;
     26 import android.media.AudioManager;
     27 import android.net.Uri;
     28 import android.text.TextUtils;
     29 import android.util.Log;
     30 import android.util.TypedValue;
     31 import android.view.KeyEvent;
     32 import android.view.LayoutInflater;
     33 import android.view.View;
     34 import android.view.ViewGroup;
     35 import android.view.WindowManager;
     36 import android.view.animation.DecelerateInterpolator;
     37 import android.view.animation.Interpolator;
     38 import android.widget.BaseAdapter;
     39 import android.widget.ImageView;
     40 import android.widget.TextView;
     41 
     42 import com.android.tv.settings.R;
     43 import com.android.tv.settings.widget.BitmapDownloader;
     44 import com.android.tv.settings.widget.BitmapDownloader.BitmapCallback;
     45 import com.android.tv.settings.widget.BitmapWorkerOptions;
     46 import com.android.tv.settings.widget.ScrollAdapter;
     47 import com.android.tv.settings.widget.ScrollAdapterBase;
     48 import com.android.tv.settings.widget.ScrollAdapterView;
     49 import com.android.tv.settings.widget.ScrollAdapterView.OnScrollListener;
     50 
     51 import java.util.ArrayList;
     52 import java.util.List;
     53 
     54 /**
     55  * Adapter class which creates actions.
     56  */
     57 public class ActionAdapter extends BaseAdapter implements ScrollAdapter,
     58         OnScrollListener, View.OnKeyListener, View.OnClickListener {
     59     private static final String TAG = "ActionAdapter";
     60 
     61     private static final boolean DEBUG = false;
     62 
     63     private static final int SELECT_ANIM_DURATION = 100;
     64     private static final int SELECT_ANIM_DELAY = 0;
     65     private static final float SELECT_ANIM_SELECTED_ALPHA = 0.2f;
     66     private static final float SELECT_ANIM_UNSELECTED_ALPHA = 1.0f;
     67     private static final float CHECKMARK_ANIM_UNSELECTED_ALPHA = 0.0f;
     68     private static final float CHECKMARK_ANIM_SELECTED_ALPHA = 1.0f;
     69 
     70     private static Integer sDescriptionMaxHeight = null;
     71 
     72     // TODO: this constant is only in KLP: update when KLP has a more standard SDK.
     73     private static final int FX_KEYPRESS_INVALID = 9; // AudioManager.FX_KEYPRESS_INVALID;
     74 
     75     /**
     76      * Object listening for adapter events.
     77      */
     78     public interface Listener {
     79 
     80         /**
     81          * Called when the user clicks on an action.
     82          */
     83         public void onActionClicked(Action action);
     84     }
     85 
     86     public interface OnFocusListener {
     87 
     88         /**
     89          * Called when the user focuses on an action.
     90          */
     91         public void onActionFocused(Action action);
     92     }
     93 
     94     /**
     95      * Object listening for adapter action select/unselect events.
     96      */
     97     public interface OnKeyListener {
     98 
     99         /**
    100          * Called when user finish selecting an action.
    101          */
    102         public void onActionSelect(Action action);
    103 
    104         /**
    105          * Called when user finish unselecting an action.
    106          */
    107         public void onActionUnselect(Action action);
    108     }
    109 
    110 
    111     private final Context mContext;
    112     private final float mUnselectedAlpha;
    113     private final float mSelectedTitleAlpha;
    114     private final float mDisabledTitleAlpha;
    115     private final float mSelectedDescriptionAlpha;
    116     private final float mDisabledDescriptionAlpha;
    117     private final float mUnselectedDescriptionAlpha;
    118     private final float mSelectedChevronAlpha;
    119     private final float mDisabledChevronAlpha;
    120     private final List<Action> mActions;
    121     private Listener mListener;
    122     private OnFocusListener mOnFocusListener;
    123     private OnKeyListener mOnKeyListener;
    124     private boolean mKeyPressed;
    125     private ScrollAdapterView mScrollAdapterView;
    126     private final int mAnimationDuration;
    127     private View mSelectedView = null;
    128 
    129     public ActionAdapter(Context context) {
    130         super();
    131         mContext = context;
    132         final Resources res = context.getResources();
    133 
    134         mAnimationDuration = res.getInteger(R.integer.dialog_animation_duration);
    135         mUnselectedAlpha = getFloat(R.dimen.list_item_unselected_text_alpha);
    136 
    137         mSelectedTitleAlpha = getFloat(R.dimen.list_item_selected_title_text_alpha);
    138         mDisabledTitleAlpha = getFloat(R.dimen.list_item_disabled_title_text_alpha);
    139 
    140         mSelectedDescriptionAlpha = getFloat(R.dimen.list_item_selected_description_text_alpha);
    141         mUnselectedDescriptionAlpha = getFloat(R.dimen.list_item_unselected_description_text_alpha);
    142         mDisabledDescriptionAlpha = getFloat(R.dimen.list_item_disabled_description_text_alpha);
    143 
    144         mSelectedChevronAlpha = getFloat(R.dimen.list_item_selected_chevron_background_alpha);
    145         mDisabledChevronAlpha = getFloat(R.dimen.list_item_disabled_chevron_background_alpha);
    146 
    147         mActions = new ArrayList<>();
    148         mKeyPressed = false;
    149     }
    150 
    151     @Override
    152     public void viewRemoved(View view) {
    153         // Do nothing.
    154     }
    155 
    156     @Override
    157     public View getScrapView(ViewGroup parent) {
    158         LayoutInflater inflater = LayoutInflater.from(mContext);
    159         View view = inflater.inflate(R.layout.settings_list_item, parent, false);
    160         return view;
    161     }
    162 
    163     @Override
    164     public int getCount() {
    165         return mActions.size();
    166     }
    167 
    168     @Override
    169     public Object getItem(int position) {
    170         return mActions.get(position);
    171     }
    172 
    173     @Override
    174     public long getItemId(int position) {
    175         return position;
    176     }
    177 
    178     @Override
    179     public boolean hasStableIds() {
    180         return true;
    181     }
    182 
    183     @Override
    184     public View getView(int position, View convertView, ViewGroup parent) {
    185         if (convertView == null) {
    186             convertView = getScrapView(parent);
    187         }
    188         Action action = mActions.get(position);
    189         TextView title = (TextView) convertView.findViewById(R.id.action_title);
    190         TextView description = (TextView) convertView.findViewById(R.id.action_description);
    191         description.setText(action.getDescription());
    192         description.setVisibility(
    193                 TextUtils.isEmpty(action.getDescription()) ? View.GONE : View.VISIBLE);
    194         title.setText(action.getTitle());
    195         ImageView checkmarkView = (ImageView) convertView.findViewById(R.id.action_checkmark);
    196         checkmarkView.setVisibility(action.isChecked() ? View.VISIBLE : View.INVISIBLE);
    197 
    198         ImageView indicatorView = (ImageView) convertView.findViewById(R.id.action_icon);
    199         setIndicator(indicatorView, action);
    200 
    201         ImageView chevronView = (ImageView) convertView.findViewById(R.id.action_next_chevron);
    202         chevronView.setVisibility(action.hasNext() ? View.VISIBLE : View.GONE);
    203 
    204         View chevronBackgroundView = convertView.findViewById(R.id.action_next_chevron_background);
    205         chevronBackgroundView.setVisibility(action.hasNext() ? View.VISIBLE : View.INVISIBLE);
    206 
    207         final Resources res = convertView.getContext().getResources();
    208         if (action.hasMultilineDescription()) {
    209             title.setMaxLines(res.getInteger(R.integer.action_title_max_lines));
    210             description.setMaxHeight(
    211                     getDescriptionMaxHeight(convertView.getContext(), title, description));
    212         } else {
    213             title.setMaxLines(res.getInteger(R.integer.action_title_min_lines));
    214             description.setMaxLines(
    215                     res.getInteger(R.integer.action_description_min_lines));
    216         }
    217 
    218         convertView.setTag(R.id.action_title, action);
    219         convertView.setOnKeyListener(this);
    220         convertView.setOnClickListener(this);
    221         changeFocus(convertView, false /* hasFocus */, false /* shouldAnimate */);
    222 
    223         return convertView;
    224     }
    225 
    226     @Override
    227     public ScrollAdapterBase getExpandAdapter() {
    228         return null;
    229     }
    230 
    231     public void setListener(Listener listener) {
    232         mListener = listener;
    233     }
    234 
    235     public void setOnFocusListener(OnFocusListener onFocusListener) {
    236         mOnFocusListener = onFocusListener;
    237     }
    238 
    239     public void setOnKeyListener(OnKeyListener onKeyListener) {
    240         mOnKeyListener = onKeyListener;
    241     }
    242 
    243     public void addAction(Action action) {
    244         mActions.add(action);
    245         notifyDataSetChanged();
    246     }
    247 
    248     /**
    249      * Used for serialization only.
    250      */
    251     public ArrayList<Action> getActions() {
    252         return new ArrayList<>(mActions);
    253     }
    254 
    255     public void setActions(ArrayList<Action> actions) {
    256         changeFocus(mSelectedView, false /* hasFocus */, false /* shouldAnimate */);
    257         mActions.clear();
    258         mActions.addAll(actions);
    259         notifyDataSetChanged();
    260     }
    261 
    262     // We want to highlight a view if we've stopped scrolling on it (mainPosition = 0).
    263     // If mainPosition is not 0, we don't want to do anything with view, but we do want to ensure
    264     // we dim the last highlighted view so that while a user is scrolling, nothing is highlighted.
    265     @Override
    266     public void onScrolled(View view, int position, float mainPosition, float secondPosition) {
    267         boolean hasFocus = (mainPosition == 0.0);
    268         if (hasFocus) {
    269             if (view != null) {
    270                 changeFocus(view, true /* hasFocus */, true /* shouldAniamte */);
    271                 mSelectedView = view;
    272             }
    273         } else if (mSelectedView != null) {
    274             changeFocus(mSelectedView, false /* hasFocus */, true /* shouldAniamte */);
    275             mSelectedView = null;
    276         }
    277     }
    278 
    279     private void changeFocus(View v, boolean hasFocus, boolean shouldAnimate) {
    280         if (v == null) {
    281             return;
    282         }
    283         Action action = (Action) v.getTag(R.id.action_title);
    284 
    285         float titleAlpha = action.isEnabled() && !action.infoOnly()
    286                 ? (hasFocus ? mSelectedTitleAlpha : mUnselectedAlpha) : mDisabledTitleAlpha;
    287         float descriptionAlpha = (!hasFocus || action.infoOnly()) ? mUnselectedDescriptionAlpha
    288                 : (action.isEnabled() ? mSelectedDescriptionAlpha : mDisabledDescriptionAlpha);
    289         float chevronAlpha = action.hasNext() && !action.infoOnly()
    290                 ? (action.isEnabled() ? mSelectedChevronAlpha : mDisabledChevronAlpha) : 0;
    291 
    292         TextView title = (TextView) v.findViewById(R.id.action_title);
    293         setAlpha(title, shouldAnimate, titleAlpha);
    294 
    295         TextView description = (TextView) v.findViewById(R.id.action_description);
    296         setAlpha(description, shouldAnimate, descriptionAlpha);
    297 
    298         ImageView checkmark = (ImageView) v.findViewById(R.id.action_checkmark);
    299         setAlpha(checkmark, shouldAnimate, titleAlpha);
    300 
    301         ImageView icon = (ImageView) v.findViewById(R.id.action_icon);
    302         setAlpha(icon, shouldAnimate, titleAlpha);
    303 
    304         View chevronBackground = v.findViewById(R.id.action_next_chevron_background);
    305         setAlpha(chevronBackground, shouldAnimate, chevronAlpha);
    306 
    307         if (mOnFocusListener != null && hasFocus) {
    308             // We still call onActionFocused so that listeners can clear state if they want.
    309             mOnFocusListener.onActionFocused((Action) v.getTag(R.id.action_title));
    310         }
    311     }
    312 
    313     private void setIndicator(final ImageView indicatorView, Action action) {
    314 
    315         Drawable indicator = action.getIndicator(mContext);
    316         if (indicator != null) {
    317             indicatorView.setImageDrawable(indicator);
    318             indicatorView.setVisibility(View.VISIBLE);
    319         } else {
    320             Uri iconUri = action.getIconUri();
    321             if (iconUri != null) {
    322                 indicatorView.setVisibility(View.INVISIBLE);
    323 
    324                 BitmapDownloader.getInstance(mContext).getBitmap(new BitmapWorkerOptions.Builder(
    325                         mContext).resource(iconUri)
    326                         .width(indicatorView.getLayoutParams().width).build(),
    327                         new BitmapCallback() {
    328                             @Override
    329                             public void onBitmapRetrieved(Bitmap bitmap) {
    330                                 if (bitmap != null) {
    331                                     indicatorView.setVisibility(View.VISIBLE);
    332                                     indicatorView.setImageBitmap(bitmap);
    333                                     fadeIn(indicatorView);
    334                                 }
    335                             }
    336                         });
    337             } else {
    338                 indicatorView.setVisibility(View.GONE);
    339             }
    340         }
    341     }
    342 
    343     private void fadeIn(View v) {
    344         v.setAlpha(0f);
    345         ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(v, "alpha", 1f);
    346         alphaAnimator.setDuration(mContext.getResources().getInteger(
    347                 android.R.integer.config_mediumAnimTime));
    348         alphaAnimator.start();
    349     }
    350 
    351     private void setAlpha(View view, boolean shouldAnimate, float alpha) {
    352         if (shouldAnimate) {
    353             view.animate().alpha(alpha)
    354                     .setDuration(mAnimationDuration)
    355                     .setInterpolator(new DecelerateInterpolator(2F))
    356                     .start();
    357         } else {
    358             view.setAlpha(alpha);
    359         }
    360     }
    361 
    362     void setScrollAdapterView(ScrollAdapterView scrollAdapterView) {
    363         mScrollAdapterView = scrollAdapterView;
    364     }
    365 
    366     @Override
    367     public void onClick(View v) {
    368         if (v != null && v.getWindowToken() != null && mListener != null) {
    369             final Action action = (Action) v.getTag(R.id.action_title);
    370             mListener.onActionClicked(action);
    371         }
    372     }
    373 
    374     /**
    375      * Now only handles KEYCODE_ENTER and KEYCODE_NUMPAD_ENTER key event.
    376      */
    377     @Override
    378     public boolean onKey(View v, int keyCode, KeyEvent event) {
    379         if (v == null) {
    380             return false;
    381         }
    382         boolean handled = false;
    383         Action action = (Action) v.getTag(R.id.action_title);
    384         switch (keyCode) {
    385             case KeyEvent.KEYCODE_DPAD_CENTER:
    386             case KeyEvent.KEYCODE_NUMPAD_ENTER:
    387             case KeyEvent.KEYCODE_BUTTON_X:
    388             case KeyEvent.KEYCODE_BUTTON_Y:
    389             case KeyEvent.KEYCODE_ENTER:
    390                 AudioManager manager = (AudioManager) v.getContext().getSystemService(
    391                         Context.AUDIO_SERVICE);
    392                 if (!action.isEnabled() || action.infoOnly()) {
    393                     if (v.isSoundEffectsEnabled() && event.getAction() == KeyEvent.ACTION_DOWN) {
    394                         manager.playSoundEffect(FX_KEYPRESS_INVALID);
    395                     }
    396                     return true;
    397                 }
    398 
    399                 switch (event.getAction()) {
    400                     case KeyEvent.ACTION_DOWN:
    401                         if (!mKeyPressed) {
    402                             mKeyPressed = true;
    403 
    404                             if (v.isSoundEffectsEnabled()) {
    405                                 manager.playSoundEffect(AudioManager.FX_KEY_CLICK);
    406                             }
    407 
    408                             if (DEBUG) {
    409                                 Log.d(TAG, "Enter Key down");
    410                             }
    411 
    412                             prepareAndAnimateView(v, SELECT_ANIM_UNSELECTED_ALPHA,
    413                                     SELECT_ANIM_SELECTED_ALPHA, SELECT_ANIM_DURATION,
    414                                     SELECT_ANIM_DELAY, null, mKeyPressed);
    415                             handled = true;
    416                         }
    417                         break;
    418                     case KeyEvent.ACTION_UP:
    419                         if (mKeyPressed) {
    420                             mKeyPressed = false;
    421 
    422                             if (DEBUG) {
    423                                 Log.d(TAG, "Enter Key up");
    424                             }
    425 
    426                             prepareAndAnimateView(v, SELECT_ANIM_SELECTED_ALPHA,
    427                                     SELECT_ANIM_UNSELECTED_ALPHA, SELECT_ANIM_DURATION,
    428                                     SELECT_ANIM_DELAY, null, mKeyPressed);
    429                             handled = true;
    430                         }
    431                         break;
    432                     default:
    433                         break;
    434                 }
    435                 break;
    436             default:
    437                 break;
    438         }
    439         return handled;
    440     }
    441 
    442     private void prepareAndAnimateView(final View v, float initAlpha, float destAlpha, int duration,
    443             int delay, Interpolator interpolator, final boolean pressed) {
    444         if (v != null && v.getWindowToken() != null) {
    445             final Action action = (Action) v.getTag(R.id.action_title);
    446 
    447             if (!pressed) {
    448                 fadeCheckmarks(v, action, duration, delay, interpolator);
    449             }
    450 
    451             v.setAlpha(initAlpha);
    452             v.setLayerType(View.LAYER_TYPE_HARDWARE, null);
    453             v.buildLayer();
    454             v.animate().alpha(destAlpha).setDuration(duration).setStartDelay(delay);
    455             if (interpolator != null) {
    456                 v.animate().setInterpolator(interpolator);
    457             }
    458             v.animate().setListener(new AnimatorListenerAdapter() {
    459                 @Override
    460                 public void onAnimationEnd(Animator animation) {
    461 
    462                     v.setLayerType(View.LAYER_TYPE_NONE, null);
    463                     if (pressed) {
    464                         if (mOnKeyListener != null) {
    465                             mOnKeyListener.onActionSelect(action);
    466                         }
    467                     } else {
    468                         if (mOnKeyListener != null) {
    469                             mOnKeyListener.onActionUnselect(action);
    470                         }
    471                         if (mListener != null) {
    472                             mListener.onActionClicked(action);
    473                         }
    474                     }
    475                 }
    476             });
    477             v.animate().start();
    478         }
    479     }
    480 
    481     private void fadeCheckmarks(final View v, final Action action, int duration, int delay,
    482             Interpolator interpolator) {
    483         int actionCheckSetId = action.getCheckSetId();
    484         if (actionCheckSetId != Action.NO_CHECK_SET) {
    485             // Find any actions that are checked and are in the same group
    486             // as the selected action. Fade their checkmarks out.
    487             for (int i = 0, size = mActions.size(); i < size; i++) {
    488                 Action a = mActions.get(i);
    489                 if (a != action && a.getCheckSetId() == actionCheckSetId && a.isChecked()) {
    490                     a.setChecked(false);
    491                     if (mScrollAdapterView != null) {
    492                         View viewToAnimateOut = mScrollAdapterView.getItemView(i);
    493                         if (viewToAnimateOut != null) {
    494                             final View checkView = viewToAnimateOut.findViewById(
    495                                     R.id.action_checkmark);
    496                             checkView.animate().alpha(CHECKMARK_ANIM_UNSELECTED_ALPHA)
    497                                     .setDuration(duration).setStartDelay(delay);
    498                             if (interpolator != null) {
    499                                 checkView.animate().setInterpolator(interpolator);
    500                             }
    501                             checkView.animate().setListener(new AnimatorListenerAdapter() {
    502                                 @Override
    503                                 public void onAnimationEnd(Animator animation) {
    504                                     checkView.setVisibility(View.INVISIBLE);
    505                                 }
    506                             });
    507                         }
    508                     }
    509                 }
    510             }
    511 
    512             // If we we'ren't already checked, fade our checkmark in.
    513             if (!action.isChecked()) {
    514                 action.setChecked(true);
    515                 if (mScrollAdapterView != null) {
    516                     final View checkView = v.findViewById(R.id.action_checkmark);
    517                     checkView.setVisibility(View.VISIBLE);
    518                     checkView.setAlpha(CHECKMARK_ANIM_UNSELECTED_ALPHA);
    519                     checkView.animate().alpha(CHECKMARK_ANIM_SELECTED_ALPHA).setDuration(duration)
    520                             .setStartDelay(delay);
    521                     if (interpolator != null) {
    522                         checkView.animate().setInterpolator(interpolator);
    523                     }
    524                     checkView.animate().setListener(null);
    525                 }
    526             }
    527         }
    528     }
    529 
    530     /**
    531      * @return the max height in pixels the description can be such that the
    532      * action nicely takes up the entire screen.
    533      */
    534     private static Integer getDescriptionMaxHeight(Context context, TextView title,
    535             TextView description) {
    536         if (sDescriptionMaxHeight == null) {
    537             final Resources res = context.getResources();
    538             final float verticalPadding = res.getDimension(R.dimen.list_item_vertical_padding);
    539             final int titleMaxLines = res.getInteger(R.integer.action_title_max_lines);
    540             final int displayHeight = ((WindowManager) context.getSystemService(
    541                     Context.WINDOW_SERVICE)).getDefaultDisplay().getHeight();
    542 
    543             // The 2 multiplier on the title height calculation is a conservative
    544             // estimate for font padding which can not be calculated at this stage
    545             // since the view hasn't been rendered yet.
    546             sDescriptionMaxHeight = (int) (displayHeight -
    547                     2 * verticalPadding - 2 * titleMaxLines * title.getLineHeight());
    548         }
    549         return sDescriptionMaxHeight;
    550     }
    551 
    552     private float getFloat(int resourceId) {
    553         TypedValue buffer = new TypedValue();
    554         mContext.getResources().getValue(resourceId, buffer, true);
    555         return buffer.getFloat();
    556     }
    557 }
    558