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