1/*
2 * Copyright (C) 2014 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.app.Activity;
17import android.content.Context;
18import android.graphics.Color;
19import android.graphics.drawable.ColorDrawable;
20import android.graphics.drawable.Drawable;
21import android.os.Handler;
22import android.support.annotation.ColorInt;
23import android.support.v17.leanback.R;
24import android.support.v7.widget.RecyclerView;
25import android.util.Log;
26import android.util.TypedValue;
27import android.view.KeyEvent;
28import android.view.LayoutInflater;
29import android.view.View;
30import android.view.ViewGroup;
31import android.widget.FrameLayout;
32import android.widget.ImageView;
33
34/**
35 * Renders a {@link DetailsOverviewRow} to display an overview of an item.
36 * Typically this row will be the first row in a fragment
37 * such as the {@link android.support.v17.leanback.app.DetailsFragment
38 * DetailsFragment}.  The View created by the DetailsOverviewRowPresenter is made in three parts:
39 * ImageView on the left, action list view on the bottom and a customizable detailed
40 * description view on the right.
41 *
42 * <p>The detailed description is rendered using a {@link Presenter} passed in
43 * {@link #DetailsOverviewRowPresenter(Presenter)}.  Typically this will be an instance of
44 * {@link AbstractDetailsDescriptionPresenter}.  The application can access the
45 * detailed description ViewHolder from {@link ViewHolder#mDetailsDescriptionViewHolder}.
46 * </p>
47 *
48 * <p>
49 * To participate in activity transition, call {@link #setSharedElementEnterTransition(Activity,
50 * String)} during Activity's onCreate().
51 * </p>
52 *
53 * <p>
54 * Because transition support and layout are fully controlled by DetailsOverviewRowPresenter,
55 * developer can not override DetailsOverviewRowPresenter.ViewHolder for adding/replacing views
56 * of DetailsOverviewRowPresenter.  If further customization is required beyond replacing
57 * the detailed description, the application should create a new row presenter class.
58 * </p>
59 * @deprecated  Use {@link FullWidthDetailsOverviewRowPresenter}
60 */
61@Deprecated
62public class DetailsOverviewRowPresenter extends RowPresenter {
63
64    static final String TAG = "DetailsOverviewRowPresenter";
65    static final boolean DEBUG = false;
66
67    private static final int MORE_ACTIONS_FADE_MS = 100;
68    private static final long DEFAULT_TIMEOUT = 5000;
69
70    class ActionsItemBridgeAdapter extends ItemBridgeAdapter {
71        DetailsOverviewRowPresenter.ViewHolder mViewHolder;
72
73        ActionsItemBridgeAdapter(DetailsOverviewRowPresenter.ViewHolder viewHolder) {
74            mViewHolder = viewHolder;
75        }
76
77        @Override
78        public void onBind(final ItemBridgeAdapter.ViewHolder ibvh) {
79            if (mViewHolder.getOnItemViewClickedListener() != null
80                    || mActionClickedListener != null) {
81                ibvh.getPresenter().setOnClickListener(
82                        ibvh.getViewHolder(), new View.OnClickListener() {
83                            @Override
84                            public void onClick(View v) {
85                                if (mViewHolder.getOnItemViewClickedListener() != null) {
86                                    mViewHolder.getOnItemViewClickedListener().onItemClicked(
87                                            ibvh.getViewHolder(), ibvh.getItem(),
88                                            mViewHolder, mViewHolder.getRow());
89                                }
90                                if (mActionClickedListener != null) {
91                                    mActionClickedListener.onActionClicked((Action) ibvh.getItem());
92                                }
93                            }
94                        });
95            }
96        }
97        @Override
98        public void onUnbind(final ItemBridgeAdapter.ViewHolder ibvh) {
99            if (mViewHolder.getOnItemViewClickedListener() != null
100                    || mActionClickedListener != null) {
101                ibvh.getPresenter().setOnClickListener(ibvh.getViewHolder(), null);
102            }
103        }
104        @Override
105        public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder viewHolder) {
106            // Remove first to ensure we don't add ourselves more than once.
107            viewHolder.itemView.removeOnLayoutChangeListener(mViewHolder.mLayoutChangeListener);
108            viewHolder.itemView.addOnLayoutChangeListener(mViewHolder.mLayoutChangeListener);
109        }
110        @Override
111        public void onDetachedFromWindow(ItemBridgeAdapter.ViewHolder viewHolder) {
112            viewHolder.itemView.removeOnLayoutChangeListener(mViewHolder.mLayoutChangeListener);
113            mViewHolder.checkFirstAndLastPosition(false);
114        }
115    }
116
117    /**
118     * A ViewHolder for the DetailsOverviewRow.
119     */
120    public final class ViewHolder extends RowPresenter.ViewHolder {
121        final FrameLayout mOverviewFrame;
122        final ViewGroup mOverviewView;
123        final ImageView mImageView;
124        final ViewGroup mRightPanel;
125        final FrameLayout mDetailsDescriptionFrame;
126        final HorizontalGridView mActionsRow;
127        public final Presenter.ViewHolder mDetailsDescriptionViewHolder;
128        int mNumItems;
129        boolean mShowMoreRight;
130        boolean mShowMoreLeft;
131        ItemBridgeAdapter mActionBridgeAdapter;
132        final Handler mHandler = new Handler();
133
134        final Runnable mUpdateDrawableCallback = new Runnable() {
135            @Override
136            public void run() {
137                bindImageDrawable(ViewHolder.this);
138            }
139        };
140
141        final DetailsOverviewRow.Listener mListener = new DetailsOverviewRow.Listener() {
142            @Override
143            public void onImageDrawableChanged(DetailsOverviewRow row) {
144                mHandler.removeCallbacks(mUpdateDrawableCallback);
145                mHandler.post(mUpdateDrawableCallback);
146            }
147
148            @Override
149            public void onItemChanged(DetailsOverviewRow row) {
150                if (mDetailsDescriptionViewHolder != null) {
151                    mDetailsPresenter.onUnbindViewHolder(mDetailsDescriptionViewHolder);
152                }
153                mDetailsPresenter.onBindViewHolder(mDetailsDescriptionViewHolder, row.getItem());
154            }
155
156            @Override
157            public void onActionsAdapterChanged(DetailsOverviewRow row) {
158                bindActions(row.getActionsAdapter());
159            }
160        };
161
162        void bindActions(ObjectAdapter adapter) {
163            mActionBridgeAdapter.setAdapter(adapter);
164            mActionsRow.setAdapter(mActionBridgeAdapter);
165            mNumItems = mActionBridgeAdapter.getItemCount();
166
167            mShowMoreRight = false;
168            mShowMoreLeft = true;
169            showMoreLeft(false);
170        }
171
172        final View.OnLayoutChangeListener mLayoutChangeListener =
173                new View.OnLayoutChangeListener() {
174
175            @Override
176            public void onLayoutChange(View v, int left, int top, int right,
177                    int bottom, int oldLeft, int oldTop, int oldRight, int oldBottom) {
178                if (DEBUG) Log.v(TAG, "onLayoutChange " + v);
179                checkFirstAndLastPosition(false);
180            }
181        };
182
183        final OnChildSelectedListener mChildSelectedListener = new OnChildSelectedListener() {
184            @Override
185            public void onChildSelected(ViewGroup parent, View view, int position, long id) {
186                dispatchItemSelection(view);
187            }
188        };
189
190        void dispatchItemSelection(View view) {
191            if (!isSelected()) {
192                return;
193            }
194            ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder) (view != null
195                    ? mActionsRow.getChildViewHolder(view)
196                    : mActionsRow.findViewHolderForPosition(mActionsRow.getSelectedPosition()));
197            if (ibvh == null) {
198                if (getOnItemViewSelectedListener() != null) {
199                    getOnItemViewSelectedListener().onItemSelected(null, null,
200                            ViewHolder.this, getRow());
201                }
202            } else {
203                if (getOnItemViewSelectedListener() != null) {
204                    getOnItemViewSelectedListener().onItemSelected(ibvh.getViewHolder(), ibvh.getItem(),
205                            ViewHolder.this, getRow());
206                }
207            }
208        };
209
210        final RecyclerView.OnScrollListener mScrollListener =
211                new RecyclerView.OnScrollListener() {
212
213            @Override
214            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
215            }
216            @Override
217            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
218                checkFirstAndLastPosition(true);
219            }
220        };
221
222        private int getViewCenter(View view) {
223            return (view.getRight() - view.getLeft()) / 2;
224        }
225
226        void checkFirstAndLastPosition(boolean fromScroll) {
227            RecyclerView.ViewHolder viewHolder;
228
229            viewHolder = mActionsRow.findViewHolderForPosition(mNumItems - 1);
230            boolean showRight = (viewHolder == null
231                    || viewHolder.itemView.getRight() > mActionsRow.getWidth());
232
233            viewHolder = mActionsRow.findViewHolderForPosition(0);
234            boolean showLeft = (viewHolder == null || viewHolder.itemView.getLeft() < 0);
235
236            if (DEBUG) {
237                Log.v(TAG, "checkFirstAndLast fromScroll " + fromScroll
238                        + " showRight " + showRight + " showLeft " + showLeft);
239            }
240
241            showMoreRight(showRight);
242            showMoreLeft(showLeft);
243        }
244
245        private void showMoreLeft(boolean show) {
246            if (show != mShowMoreLeft) {
247                mActionsRow.setFadingLeftEdge(show);
248                mShowMoreLeft = show;
249            }
250        }
251
252        private void showMoreRight(boolean show) {
253            if (show != mShowMoreRight) {
254                mActionsRow.setFadingRightEdge(show);
255                mShowMoreRight = show;
256            }
257        }
258
259        /**
260         * Constructor for the ViewHolder.
261         *
262         * @param rootView The root View that this view holder will be attached
263         *        to.
264         */
265        public ViewHolder(View rootView, Presenter detailsPresenter) {
266            super(rootView);
267            mOverviewFrame = (FrameLayout) rootView.findViewById(R.id.details_frame);
268            mOverviewView = (ViewGroup) rootView.findViewById(R.id.details_overview);
269            mImageView = (ImageView) rootView.findViewById(R.id.details_overview_image);
270            mRightPanel = (ViewGroup) rootView.findViewById(R.id.details_overview_right_panel);
271            mDetailsDescriptionFrame =
272                    (FrameLayout) mRightPanel.findViewById(R.id.details_overview_description);
273            mActionsRow =
274                    (HorizontalGridView) mRightPanel.findViewById(R.id.details_overview_actions);
275            mActionsRow.setHasOverlappingRendering(false);
276            mActionsRow.setOnScrollListener(mScrollListener);
277            mActionsRow.setAdapter(mActionBridgeAdapter);
278            mActionsRow.setOnChildSelectedListener(mChildSelectedListener);
279
280            final int fadeLength = rootView.getResources().getDimensionPixelSize(
281                    R.dimen.lb_details_overview_actions_fade_size);
282            mActionsRow.setFadingRightEdgeLength(fadeLength);
283            mActionsRow.setFadingLeftEdgeLength(fadeLength);
284            mDetailsDescriptionViewHolder =
285                    detailsPresenter.onCreateViewHolder(mDetailsDescriptionFrame);
286            mDetailsDescriptionFrame.addView(mDetailsDescriptionViewHolder.view);
287        }
288    }
289
290    final Presenter mDetailsPresenter;
291    OnActionClickedListener mActionClickedListener;
292
293    private int mBackgroundColor = Color.TRANSPARENT;
294    private boolean mBackgroundColorSet;
295    private boolean mIsStyleLarge = true;
296
297    private DetailsOverviewSharedElementHelper mSharedElementHelper;
298
299    /**
300     * Constructor for a DetailsOverviewRowPresenter.
301     *
302     * @param detailsPresenter The {@link Presenter} used to render the detailed
303     *        description of the row.
304     */
305    public DetailsOverviewRowPresenter(Presenter detailsPresenter) {
306        setHeaderPresenter(null);
307        setSelectEffectEnabled(false);
308        mDetailsPresenter = detailsPresenter;
309    }
310
311    /**
312     * Sets the listener for Action click events.
313     */
314    public void setOnActionClickedListener(OnActionClickedListener listener) {
315        mActionClickedListener = listener;
316    }
317
318    /**
319     * Returns the listener for Action click events.
320     */
321    public OnActionClickedListener getOnActionClickedListener() {
322        return mActionClickedListener;
323    }
324
325    /**
326     * Sets the background color.  If not set, a default from the theme will be used.
327     */
328    public void setBackgroundColor(@ColorInt int color) {
329        mBackgroundColor = color;
330        mBackgroundColorSet = true;
331    }
332
333    /**
334     * Returns the background color.  If no background color was set, transparent
335     * is returned.
336     */
337    @ColorInt
338    public int getBackgroundColor() {
339        return mBackgroundColor;
340    }
341
342    /**
343     * Sets the layout style to be large or small. This affects the height of
344     * the overview, including the text description. The default is large.
345     */
346    public void setStyleLarge(boolean large) {
347        mIsStyleLarge = large;
348    }
349
350    /**
351     * Returns true if the layout style is large.
352     */
353    public boolean isStyleLarge() {
354        return mIsStyleLarge;
355    }
356
357    /**
358     * Sets the enter transition of target activity to be
359     * transiting into overview row created by this presenter.  The transition will
360     * be cancelled if the overview image is not loaded in the timeout period.
361     * <p>
362     * It assumes shared element passed from calling activity is an ImageView;
363     * the shared element transits to overview image on the starting edge of the detail
364     * overview row, while bounds of overview row grows and reveals text
365     * and action buttons.
366     * <p>
367     * The method must be invoked in target Activity's onCreate().
368     */
369    public final void setSharedElementEnterTransition(Activity activity,
370            String sharedElementName, long timeoutMs) {
371        if (mSharedElementHelper == null) {
372            mSharedElementHelper = new DetailsOverviewSharedElementHelper();
373        }
374        mSharedElementHelper.setSharedElementEnterTransition(activity, sharedElementName,
375                timeoutMs);
376    }
377
378    /**
379     * Sets the enter transition of target activity to be
380     * transiting into overview row created by this presenter.  The transition will
381     * be cancelled if overview image is not loaded in a default timeout period.
382     * <p>
383     * It assumes shared element passed from calling activity is an ImageView;
384     * the shared element transits to overview image on the starting edge of the detail
385     * overview row, while bounds of overview row grows and reveals text
386     * and action buttons.
387     * <p>
388     * The method must be invoked in target Activity's onCreate().
389     */
390    public final void setSharedElementEnterTransition(Activity activity,
391            String sharedElementName) {
392        setSharedElementEnterTransition(activity, sharedElementName, DEFAULT_TIMEOUT);
393    }
394
395    private int getDefaultBackgroundColor(Context context) {
396        TypedValue outValue = new TypedValue();
397        if (context.getTheme().resolveAttribute(R.attr.defaultBrandColor, outValue, true)) {
398            return context.getResources().getColor(outValue.resourceId);
399        }
400        return context.getResources().getColor(R.color.lb_default_brand_color);
401    }
402
403    @Override
404    protected void onRowViewSelected(RowPresenter.ViewHolder vh, boolean selected) {
405        super.onRowViewSelected(vh, selected);
406        if (selected) {
407            ((ViewHolder) vh).dispatchItemSelection(null);
408        }
409    }
410
411    @Override
412    protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) {
413        View v = LayoutInflater.from(parent.getContext())
414            .inflate(R.layout.lb_details_overview, parent, false);
415        ViewHolder vh = new ViewHolder(v, mDetailsPresenter);
416
417        initDetailsOverview(vh);
418
419        return vh;
420    }
421
422    private int getCardHeight(Context context) {
423        int resId = mIsStyleLarge ? R.dimen.lb_details_overview_height_large :
424            R.dimen.lb_details_overview_height_small;
425        return context.getResources().getDimensionPixelSize(resId);
426    }
427
428    private void initDetailsOverview(final ViewHolder vh) {
429        vh.mActionBridgeAdapter = new ActionsItemBridgeAdapter(vh);
430        final View overview = vh.mOverviewFrame;
431        ViewGroup.LayoutParams lp = overview.getLayoutParams();
432        lp.height = getCardHeight(overview.getContext());
433        overview.setLayoutParams(lp);
434
435        if (!getSelectEffectEnabled()) {
436            vh.mOverviewFrame.setForeground(null);
437        }
438        vh.mActionsRow.setOnUnhandledKeyListener(new BaseGridView.OnUnhandledKeyListener() {
439            @Override
440            public boolean onUnhandledKey(KeyEvent event) {
441                if (vh.getOnKeyListener() != null) {
442                    if (vh.getOnKeyListener().onKey(vh.view, event.getKeyCode(), event)) {
443                        return true;
444                    }
445                }
446                return false;
447            }
448        });
449    }
450
451    private static int getNonNegativeWidth(Drawable drawable) {
452        final int width = (drawable == null) ? 0 : drawable.getIntrinsicWidth();
453        return (width > 0 ? width : 0);
454    }
455
456    private static int getNonNegativeHeight(Drawable drawable) {
457        final int height = (drawable == null) ? 0 : drawable.getIntrinsicHeight();
458        return (height > 0 ? height : 0);
459    }
460
461    void bindImageDrawable(ViewHolder vh) {
462        DetailsOverviewRow row = (DetailsOverviewRow) vh.getRow();
463
464        ViewGroup.MarginLayoutParams layoutParams =
465                (ViewGroup.MarginLayoutParams) vh.mImageView.getLayoutParams();
466        final int cardHeight = getCardHeight(vh.mImageView.getContext());
467        final int verticalMargin = vh.mImageView.getResources().getDimensionPixelSize(
468                R.dimen.lb_details_overview_image_margin_vertical);
469        final int horizontalMargin = vh.mImageView.getResources().getDimensionPixelSize(
470                R.dimen.lb_details_overview_image_margin_horizontal);
471        final int drawableWidth = getNonNegativeWidth(row.getImageDrawable());
472        final int drawableHeight = getNonNegativeHeight(row.getImageDrawable());
473
474        boolean scaleImage = row.isImageScaleUpAllowed();
475        boolean useMargin = false;
476
477        if (row.getImageDrawable() != null) {
478            boolean landscape = false;
479
480            // If large style and landscape image we always use margin.
481            if (drawableWidth > drawableHeight) {
482                landscape = true;
483                if (mIsStyleLarge) {
484                    useMargin = true;
485                }
486            }
487            // If long dimension bigger than the card height we scale down.
488            if ((landscape && drawableWidth > cardHeight)
489                    || (!landscape && drawableHeight > cardHeight)) {
490                scaleImage = true;
491            }
492            // If we're not scaling to fit the card height then we always use margin.
493            if (!scaleImage) {
494                useMargin = true;
495            }
496            // If using margin than may need to scale down.
497            if (useMargin && !scaleImage) {
498                if (landscape && drawableWidth > cardHeight - horizontalMargin) {
499                    scaleImage = true;
500                } else if (!landscape && drawableHeight > cardHeight - 2 * verticalMargin) {
501                    scaleImage = true;
502                }
503            }
504        }
505
506        final int bgColor = mBackgroundColorSet ? mBackgroundColor :
507            getDefaultBackgroundColor(vh.mOverviewView.getContext());
508
509        if (useMargin) {
510            layoutParams.setMarginStart(horizontalMargin);
511            layoutParams.topMargin = layoutParams.bottomMargin = verticalMargin;
512            vh.mOverviewFrame.setBackgroundColor(bgColor);
513            vh.mRightPanel.setBackground(null);
514            vh.mImageView.setBackground(null);
515        } else {
516            layoutParams.leftMargin = layoutParams.topMargin = layoutParams.bottomMargin = 0;
517            vh.mRightPanel.setBackgroundColor(bgColor);
518            vh.mImageView.setBackgroundColor(bgColor);
519            vh.mOverviewFrame.setBackground(null);
520        }
521        RoundedRectHelper.getInstance().setClipToRoundedOutline(vh.mOverviewFrame, true);
522
523        if (scaleImage) {
524            vh.mImageView.setScaleType(ImageView.ScaleType.FIT_START);
525            vh.mImageView.setAdjustViewBounds(true);
526            vh.mImageView.setMaxWidth(cardHeight);
527            layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
528            layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
529        } else {
530            vh.mImageView.setScaleType(ImageView.ScaleType.CENTER);
531            vh.mImageView.setAdjustViewBounds(false);
532            layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
533            // Limit width to the card height
534            layoutParams.width = Math.min(cardHeight, drawableWidth);
535        }
536        vh.mImageView.setLayoutParams(layoutParams);
537        vh.mImageView.setImageDrawable(row.getImageDrawable());
538        if (row.getImageDrawable() != null && mSharedElementHelper != null) {
539            mSharedElementHelper.onBindToDrawable(vh);
540        }
541    }
542
543    @Override
544    protected void onBindRowViewHolder(RowPresenter.ViewHolder holder, Object item) {
545        super.onBindRowViewHolder(holder, item);
546
547        DetailsOverviewRow row = (DetailsOverviewRow) item;
548        ViewHolder vh = (ViewHolder) holder;
549
550        bindImageDrawable(vh);
551        mDetailsPresenter.onBindViewHolder(vh.mDetailsDescriptionViewHolder, row.getItem());
552        vh.bindActions(row.getActionsAdapter());
553        row.addListener(vh.mListener);
554    }
555
556    @Override
557    protected void onUnbindRowViewHolder(RowPresenter.ViewHolder holder) {
558        ViewHolder vh = (ViewHolder) holder;
559        DetailsOverviewRow dor = (DetailsOverviewRow) vh.getRow();
560        dor.removeListener(vh.mListener);
561        if (vh.mDetailsDescriptionViewHolder != null) {
562            mDetailsPresenter.onUnbindViewHolder(vh.mDetailsDescriptionViewHolder);
563        }
564        super.onUnbindRowViewHolder(holder);
565    }
566
567    @Override
568    public final boolean isUsingDefaultSelectEffect() {
569        return false;
570    }
571
572    @Override
573    protected void onSelectLevelChanged(RowPresenter.ViewHolder holder) {
574        super.onSelectLevelChanged(holder);
575        if (getSelectEffectEnabled()) {
576            ViewHolder vh = (ViewHolder) holder;
577            int dimmedColor = vh.mColorDimmer.getPaint().getColor();
578            ((ColorDrawable) vh.mOverviewFrame.getForeground().mutate()).setColor(dimmedColor);
579        }
580    }
581
582    @Override
583    protected void onRowViewAttachedToWindow(RowPresenter.ViewHolder vh) {
584        super.onRowViewAttachedToWindow(vh);
585        if (mDetailsPresenter != null) {
586            mDetailsPresenter.onViewAttachedToWindow(
587                    ((ViewHolder) vh).mDetailsDescriptionViewHolder);
588        }
589    }
590
591    @Override
592    protected void onRowViewDetachedFromWindow(RowPresenter.ViewHolder vh) {
593        super.onRowViewDetachedFromWindow(vh);
594        if (mDetailsPresenter != null) {
595            mDetailsPresenter.onViewDetachedFromWindow(
596                    ((ViewHolder) vh).mDetailsDescriptionViewHolder);
597        }
598    }
599}
600