Home | History | Annotate | Download | only in widget
      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.printspooler.widget;
     18 
     19 import android.content.Context;
     20 import android.support.v4.widget.ViewDragHelper;
     21 import android.util.AttributeSet;
     22 import android.view.MotionEvent;
     23 import android.view.View;
     24 import android.view.ViewGroup;
     25 import android.view.inputmethod.InputMethodManager;
     26 
     27 import com.android.printspooler.R;
     28 
     29 /**
     30  * This class is a layout manager for the print screen. It has a sliding
     31  * area that contains the print options. If the sliding area is open the
     32  * print options are visible and if it is closed a summary of the print
     33  * job is shown. Under the sliding area there is a place for putting
     34  * arbitrary content such as preview, error message, progress indicator,
     35  * etc. The sliding area is covering the content holder under it when
     36  * the former is opened.
     37  */
     38 @SuppressWarnings("unused")
     39 public final class PrintContentView extends ViewGroup implements View.OnClickListener {
     40     private static final int FIRST_POINTER_ID = 0;
     41 
     42     private static final int ALPHA_MASK = 0xff000000;
     43     private static final int ALPHA_SHIFT = 24;
     44 
     45     private static final int COLOR_MASK = 0xffffff;
     46 
     47     private final ViewDragHelper mDragger;
     48 
     49     private final int mScrimColor;
     50 
     51     private View mStaticContent;
     52     private ViewGroup mSummaryContent;
     53     private View mDynamicContent;
     54 
     55     private View mDraggableContent;
     56     private View mPrintButton;
     57     private View mMoreOptionsButton;
     58     private ViewGroup mOptionsContainer;
     59 
     60     private View mEmbeddedContentContainer;
     61     private View mEmbeddedContentScrim;
     62 
     63     private View mExpandCollapseHandle;
     64     private View mExpandCollapseIcon;
     65 
     66     private int mClosedOptionsOffsetY;
     67     private int mCurrentOptionsOffsetY = Integer.MIN_VALUE;
     68 
     69     private OptionsStateChangeListener mOptionsStateChangeListener;
     70 
     71     private OptionsStateController mOptionsStateController;
     72 
     73     private int mOldDraggableHeight;
     74 
     75     private float mDragProgress;
     76 
     77     public interface OptionsStateChangeListener {
     78         public void onOptionsOpened();
     79         public void onOptionsClosed();
     80     }
     81 
     82     public interface OptionsStateController {
     83         public boolean canOpenOptions();
     84         public boolean canCloseOptions();
     85     }
     86 
     87     public PrintContentView(Context context, AttributeSet attrs) {
     88         super(context, attrs);
     89         mDragger = ViewDragHelper.create(this, new DragCallbacks());
     90 
     91         mScrimColor = context.getColor(R.color.print_preview_scrim_color);
     92 
     93         // The options view is sliding under the static header but appears
     94         // after it in the layout, so we will draw in opposite order.
     95         setChildrenDrawingOrderEnabled(true);
     96     }
     97 
     98     public void setOptionsStateChangeListener(OptionsStateChangeListener listener) {
     99         mOptionsStateChangeListener = listener;
    100     }
    101 
    102     public void setOpenOptionsController(OptionsStateController controller) {
    103         mOptionsStateController = controller;
    104     }
    105 
    106     public boolean isOptionsOpened() {
    107         return mCurrentOptionsOffsetY == 0;
    108     }
    109 
    110     private boolean isOptionsClosed() {
    111         return mCurrentOptionsOffsetY == mClosedOptionsOffsetY;
    112     }
    113 
    114     public void openOptions() {
    115         if (isOptionsOpened()) {
    116             return;
    117         }
    118         mDragger.smoothSlideViewTo(mDynamicContent, mDynamicContent.getLeft(),
    119                 getOpenedOptionsY());
    120         invalidate();
    121     }
    122 
    123     public void closeOptions() {
    124         if (isOptionsClosed()) {
    125             return;
    126         }
    127         mDragger.smoothSlideViewTo(mDynamicContent, mDynamicContent.getLeft(),
    128                 getClosedOptionsY());
    129         invalidate();
    130     }
    131 
    132     @Override
    133     protected int getChildDrawingOrder(int childCount, int i) {
    134         return childCount - i - 1;
    135     }
    136 
    137     @Override
    138     protected void onFinishInflate() {
    139         mStaticContent = findViewById(R.id.static_content);
    140         mSummaryContent = findViewById(R.id.summary_content);
    141         mDynamicContent = findViewById(R.id.dynamic_content);
    142         mDraggableContent = findViewById(R.id.draggable_content);
    143         mPrintButton = findViewById(R.id.print_button);
    144         mMoreOptionsButton = findViewById(R.id.more_options_button);
    145         mOptionsContainer = findViewById(R.id.options_container);
    146         mEmbeddedContentContainer = findViewById(R.id.embedded_content_container);
    147         mEmbeddedContentScrim = findViewById(R.id.embedded_content_scrim);
    148         mExpandCollapseHandle = findViewById(R.id.expand_collapse_handle);
    149         mExpandCollapseIcon = findViewById(R.id.expand_collapse_icon);
    150 
    151         mExpandCollapseHandle.setOnClickListener(this);
    152         mSummaryContent.setOnClickListener(this);
    153 
    154         // Make sure we start in a closed options state.
    155         onDragProgress(1.0f);
    156 
    157         // The framework gives focus to the frist focusable and we
    158         // do not want that, hence we will take focus instead.
    159         setFocusableInTouchMode(true);
    160     }
    161 
    162     @Override
    163     public void focusableViewAvailable(View v) {
    164         // The framework gives focus to the frist focusable and we
    165         // do not want that, hence do not announce new focusables.
    166         return;
    167     }
    168 
    169     @Override
    170     public void onClick(View view) {
    171         if (view == mExpandCollapseHandle || view == mSummaryContent) {
    172             if (isOptionsClosed() && mOptionsStateController.canOpenOptions()) {
    173                 openOptions();
    174             } else if (isOptionsOpened() && mOptionsStateController.canCloseOptions()) {
    175                 closeOptions();
    176             } // else in open/close progress do nothing.
    177         } else if (view == mEmbeddedContentScrim) {
    178             if (isOptionsOpened() && mOptionsStateController.canCloseOptions()) {
    179                 closeOptions();
    180             }
    181         }
    182     }
    183 
    184     @Override
    185     public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
    186         /* do nothing */
    187     }
    188 
    189     @Override
    190     public boolean onTouchEvent(MotionEvent event) {
    191         mDragger.processTouchEvent(event);
    192         return true;
    193     }
    194 
    195     @Override
    196     public boolean onInterceptTouchEvent(MotionEvent event) {
    197         return mDragger.shouldInterceptTouchEvent(event)
    198                 || super.onInterceptTouchEvent(event);
    199     }
    200 
    201     @Override
    202     public void computeScroll() {
    203         if (mDragger.continueSettling(true)) {
    204             postInvalidateOnAnimation();
    205         }
    206     }
    207 
    208     private int computeScrimColor() {
    209         final int baseAlpha = (mScrimColor & ALPHA_MASK) >>> ALPHA_SHIFT;
    210         final int adjustedAlpha = (int) (baseAlpha * (1 - mDragProgress));
    211         return adjustedAlpha << ALPHA_SHIFT | (mScrimColor & COLOR_MASK);
    212     }
    213 
    214     private int getOpenedOptionsY() {
    215         return mStaticContent.getBottom();
    216     }
    217 
    218     private int getClosedOptionsY() {
    219         return getOpenedOptionsY() + mClosedOptionsOffsetY;
    220     }
    221 
    222     @Override
    223     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    224         final boolean wasOpened = isOptionsOpened();
    225 
    226         measureChild(mStaticContent, widthMeasureSpec, heightMeasureSpec);
    227 
    228         if (mSummaryContent.getVisibility() != View.GONE) {
    229             measureChild(mSummaryContent, widthMeasureSpec, heightMeasureSpec);
    230         }
    231 
    232         measureChild(mDynamicContent, widthMeasureSpec, heightMeasureSpec);
    233 
    234         measureChild(mPrintButton, widthMeasureSpec, heightMeasureSpec);
    235 
    236         // The height of the draggable content may change and if that happens
    237         // we have to adjust the sliding area closed state offset.
    238         mClosedOptionsOffsetY = mSummaryContent.getMeasuredHeight()
    239                 - mDraggableContent.getMeasuredHeight();
    240 
    241         if (mCurrentOptionsOffsetY == Integer.MIN_VALUE) {
    242             mCurrentOptionsOffsetY = mClosedOptionsOffsetY;
    243         }
    244 
    245         final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    246 
    247         // The content host must be maximally large size that fits entirely
    248         // on the screen when the options are collapsed.
    249         ViewGroup.LayoutParams params = mEmbeddedContentContainer.getLayoutParams();
    250         params.height = heightSize - mStaticContent.getMeasuredHeight()
    251                 - mSummaryContent.getMeasuredHeight() - mDynamicContent.getMeasuredHeight()
    252                 + mDraggableContent.getMeasuredHeight();
    253 
    254         // The height of the draggable content may change and if that happens
    255         // we have to adjust the current offset to ensure the sliding area is
    256         // at the correct position.
    257         if (mOldDraggableHeight != mDraggableContent.getMeasuredHeight()) {
    258             if (mOldDraggableHeight != 0) {
    259                 mCurrentOptionsOffsetY = wasOpened ? 0 : mClosedOptionsOffsetY;
    260             }
    261             mOldDraggableHeight = mDraggableContent.getMeasuredHeight();
    262         }
    263 
    264         // The content host can grow vertically as much as needed - we will be covering it.
    265         final int hostHeightMeasureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, 0);
    266         measureChild(mEmbeddedContentContainer, widthMeasureSpec, hostHeightMeasureSpec);
    267 
    268         setMeasuredDimension(resolveSize(MeasureSpec.getSize(widthMeasureSpec), widthMeasureSpec),
    269                 resolveSize(heightSize, heightMeasureSpec));
    270     }
    271 
    272     @Override
    273     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    274         mStaticContent.layout(left, top, right, mStaticContent.getMeasuredHeight());
    275 
    276         if (mSummaryContent.getVisibility() != View.GONE) {
    277             mSummaryContent.layout(left, mStaticContent.getMeasuredHeight(), right,
    278                     mStaticContent.getMeasuredHeight() + mSummaryContent.getMeasuredHeight());
    279         }
    280 
    281         final int dynContentTop = mStaticContent.getMeasuredHeight() + mCurrentOptionsOffsetY;
    282         final int dynContentBottom = dynContentTop + mDynamicContent.getMeasuredHeight();
    283 
    284         mDynamicContent.layout(left, dynContentTop, right, dynContentBottom);
    285 
    286         MarginLayoutParams params = (MarginLayoutParams) mPrintButton.getLayoutParams();
    287 
    288         final int printButtonLeft;
    289         if (getLayoutDirection() == View.LAYOUT_DIRECTION_LTR) {
    290             printButtonLeft = right - mPrintButton.getMeasuredWidth() - params.getMarginStart();
    291         } else {
    292             printButtonLeft = left + params.getMarginStart();
    293         }
    294         final int printButtonTop = dynContentBottom - mPrintButton.getMeasuredHeight() / 2;
    295         final int printButtonRight = printButtonLeft + mPrintButton.getMeasuredWidth();
    296         final int printButtonBottom = printButtonTop + mPrintButton.getMeasuredHeight();
    297 
    298         mPrintButton.layout(printButtonLeft, printButtonTop, printButtonRight, printButtonBottom);
    299 
    300         final int embContentTop = mStaticContent.getMeasuredHeight() + mClosedOptionsOffsetY
    301                 + mDynamicContent.getMeasuredHeight();
    302         final int embContentBottom = embContentTop + mEmbeddedContentContainer.getMeasuredHeight();
    303 
    304         mEmbeddedContentContainer.layout(left, embContentTop, right, embContentBottom);
    305     }
    306 
    307     @Override
    308     public LayoutParams generateLayoutParams(AttributeSet attrs) {
    309         return new ViewGroup.MarginLayoutParams(getContext(), attrs);
    310     }
    311 
    312     private void onDragProgress(float progress) {
    313         if (Float.compare(mDragProgress, progress) == 0) {
    314             return;
    315         }
    316 
    317         if ((mDragProgress == 0 && progress > 0)
    318                 || (mDragProgress == 1.0f && progress < 1.0f)) {
    319             mSummaryContent.setLayerType(View.LAYER_TYPE_HARDWARE, null);
    320             mDraggableContent.setLayerType(View.LAYER_TYPE_HARDWARE, null);
    321             mMoreOptionsButton.setLayerType(View.LAYER_TYPE_HARDWARE, null);
    322             ensureImeClosedAndInputFocusCleared();
    323         }
    324         if ((mDragProgress > 0 && progress == 0)
    325                 || (mDragProgress < 1.0f && progress == 1.0f)) {
    326             mSummaryContent.setLayerType(View.LAYER_TYPE_NONE, null);
    327             mDraggableContent.setLayerType(View.LAYER_TYPE_NONE, null);
    328             mMoreOptionsButton.setLayerType(View.LAYER_TYPE_NONE, null);
    329             mMoreOptionsButton.setLayerType(View.LAYER_TYPE_NONE, null);
    330         }
    331 
    332         mDragProgress = progress;
    333 
    334         mSummaryContent.setAlpha(progress);
    335 
    336         final float inverseAlpha = 1.0f - progress;
    337         mOptionsContainer.setAlpha(inverseAlpha);
    338         mMoreOptionsButton.setAlpha(inverseAlpha);
    339 
    340         mEmbeddedContentScrim.setBackgroundColor(computeScrimColor());
    341         if (progress == 0) {
    342             if (mOptionsStateChangeListener != null) {
    343                 mOptionsStateChangeListener.onOptionsOpened();
    344             }
    345             mExpandCollapseHandle.setContentDescription(
    346                     mContext.getString(R.string.collapse_handle));
    347             announceForAccessibility(mContext.getString(R.string.print_options_expanded));
    348             mSummaryContent.setVisibility(View.GONE);
    349             mEmbeddedContentScrim.setOnClickListener(this);
    350             mExpandCollapseIcon.setBackgroundResource(R.drawable.ic_expand_less);
    351         } else {
    352             mSummaryContent.setVisibility(View.VISIBLE);
    353         }
    354 
    355         if (progress == 1.0f) {
    356             if (mOptionsStateChangeListener != null) {
    357                 mOptionsStateChangeListener.onOptionsClosed();
    358             }
    359             mExpandCollapseHandle.setContentDescription(
    360                     mContext.getString(R.string.expand_handle));
    361             announceForAccessibility(mContext.getString(R.string.print_options_collapsed));
    362             if (mMoreOptionsButton.getVisibility() != View.GONE) {
    363                 mMoreOptionsButton.setVisibility(View.INVISIBLE);
    364             }
    365             mDraggableContent.setVisibility(View.INVISIBLE);
    366             // If we change the scrim visibility the dimming is lagging
    367             // and is janky. Now it is there but transparent, doing nothing.
    368             mEmbeddedContentScrim.setOnClickListener(null);
    369             mEmbeddedContentScrim.setClickable(false);
    370             mExpandCollapseIcon.setBackgroundResource(
    371                     com.android.internal.R.drawable.ic_expand_more);
    372         } else {
    373             if (mMoreOptionsButton.getVisibility() != View.GONE) {
    374                 mMoreOptionsButton.setVisibility(View.VISIBLE);
    375             }
    376             mDraggableContent.setVisibility(View.VISIBLE);
    377         }
    378     }
    379 
    380     private void ensureImeClosedAndInputFocusCleared() {
    381         View focused = findFocus();
    382 
    383         if (focused != null && focused.isFocused()) {
    384             InputMethodManager imm = (InputMethodManager) mContext.getSystemService(
    385                     Context.INPUT_METHOD_SERVICE);
    386             if (imm.isActive(focused)) {
    387                 imm.hideSoftInputFromWindow(getWindowToken(), 0);
    388             }
    389             focused.clearFocus();
    390         }
    391     }
    392 
    393     private final class DragCallbacks extends ViewDragHelper.Callback {
    394         @Override
    395         public boolean tryCaptureView(View child, int pointerId) {
    396             if (isOptionsOpened() && !mOptionsStateController.canCloseOptions()
    397                     || isOptionsClosed() && !mOptionsStateController.canOpenOptions()) {
    398                 return false;
    399             }
    400             return child == mDynamicContent && pointerId == FIRST_POINTER_ID;
    401         }
    402 
    403         @Override
    404         public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
    405             if ((isOptionsClosed() || isOptionsClosed()) && dy <= 0) {
    406                 return;
    407             }
    408 
    409             mCurrentOptionsOffsetY += dy;
    410             final float progress = ((float) top - getOpenedOptionsY())
    411                     / (getClosedOptionsY() - getOpenedOptionsY());
    412 
    413             mPrintButton.offsetTopAndBottom(dy);
    414 
    415             mDraggableContent.notifySubtreeAccessibilityStateChangedIfNeeded();
    416 
    417             onDragProgress(progress);
    418         }
    419 
    420         @Override
    421         public void onViewReleased(View child, float velocityX, float velocityY) {
    422             final int childTop = child.getTop();
    423 
    424             final int openedOptionsY = getOpenedOptionsY();
    425             final int closedOptionsY = getClosedOptionsY();
    426 
    427             if (childTop == openedOptionsY || childTop == closedOptionsY) {
    428                 return;
    429             }
    430 
    431             final int halfRange = closedOptionsY + (openedOptionsY - closedOptionsY) / 2;
    432             if (childTop < halfRange) {
    433                 mDragger.smoothSlideViewTo(child, child.getLeft(), closedOptionsY);
    434             } else {
    435                 mDragger.smoothSlideViewTo(child, child.getLeft(), openedOptionsY);
    436             }
    437 
    438             invalidate();
    439         }
    440 
    441         @Override
    442         public int getOrderedChildIndex(int index) {
    443             return getChildCount() - index - 1;
    444         }
    445 
    446         @Override
    447         public int getViewVerticalDragRange(View child) {
    448             return mDraggableContent.getHeight();
    449         }
    450 
    451         @Override
    452         public int clampViewPositionVertical(View child, int top, int dy) {
    453             final int staticOptionBottom = mStaticContent.getBottom();
    454             return Math.max(Math.min(top, getOpenedOptionsY()), getClosedOptionsY());
    455         }
    456     }
    457 }
    458