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 {
onOptionsOpened()78         public void onOptionsOpened();
onOptionsClosed()79         public void onOptionsClosed();
80     }
81 
82     public interface OptionsStateController {
canOpenOptions()83         public boolean canOpenOptions();
canCloseOptions()84         public boolean canCloseOptions();
85     }
86 
PrintContentView(Context context, AttributeSet attrs)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 
setOptionsStateChangeListener(OptionsStateChangeListener listener)98     public void setOptionsStateChangeListener(OptionsStateChangeListener listener) {
99         mOptionsStateChangeListener = listener;
100     }
101 
setOpenOptionsController(OptionsStateController controller)102     public void setOpenOptionsController(OptionsStateController controller) {
103         mOptionsStateController = controller;
104     }
105 
isOptionsOpened()106     public boolean isOptionsOpened() {
107         return mCurrentOptionsOffsetY == 0;
108     }
109 
isOptionsClosed()110     private boolean isOptionsClosed() {
111         return mCurrentOptionsOffsetY == mClosedOptionsOffsetY;
112     }
113 
openOptions()114     public void openOptions() {
115         if (isOptionsOpened()) {
116             return;
117         }
118         mDragger.smoothSlideViewTo(mDynamicContent, mDynamicContent.getLeft(),
119                 getOpenedOptionsY());
120         invalidate();
121     }
122 
closeOptions()123     public void closeOptions() {
124         if (isOptionsClosed()) {
125             return;
126         }
127         mDragger.smoothSlideViewTo(mDynamicContent, mDynamicContent.getLeft(),
128                 getClosedOptionsY());
129         invalidate();
130     }
131 
132     @Override
getChildDrawingOrder(int childCount, int i)133     protected int getChildDrawingOrder(int childCount, int i) {
134         return childCount - i - 1;
135     }
136 
137     @Override
onFinishInflate()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
focusableViewAvailable(View v)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
onClick(View view)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
requestDisallowInterceptTouchEvent(boolean disallowIntercept)185     public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
186         /* do nothing */
187     }
188 
189     @Override
onTouchEvent(MotionEvent event)190     public boolean onTouchEvent(MotionEvent event) {
191         mDragger.processTouchEvent(event);
192         return true;
193     }
194 
195     @Override
onInterceptTouchEvent(MotionEvent event)196     public boolean onInterceptTouchEvent(MotionEvent event) {
197         return mDragger.shouldInterceptTouchEvent(event)
198                 || super.onInterceptTouchEvent(event);
199     }
200 
201     @Override
computeScroll()202     public void computeScroll() {
203         if (mDragger.continueSettling(true)) {
204             postInvalidateOnAnimation();
205         }
206     }
207 
computeScrimColor()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 
getOpenedOptionsY()214     private int getOpenedOptionsY() {
215         return mStaticContent.getBottom();
216     }
217 
getClosedOptionsY()218     private int getClosedOptionsY() {
219         return getOpenedOptionsY() + mClosedOptionsOffsetY;
220     }
221 
222     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)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
onLayout(boolean changed, int left, int top, int right, int bottom)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
generateLayoutParams(AttributeSet attrs)308     public LayoutParams generateLayoutParams(AttributeSet attrs) {
309         return new ViewGroup.MarginLayoutParams(getContext(), attrs);
310     }
311 
onDragProgress(float progress)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 
ensureImeClosedAndInputFocusCleared()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
tryCaptureView(View child, int pointerId)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
onViewPositionChanged(View changedView, int left, int top, int dx, int dy)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
onViewReleased(View child, float velocityX, float velocityY)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
getOrderedChildIndex(int index)442         public int getOrderedChildIndex(int index) {
443             return getChildCount() - index - 1;
444         }
445 
446         @Override
getViewVerticalDragRange(View child)447         public int getViewVerticalDragRange(View child) {
448             return mDraggableContent.getHeight();
449         }
450 
451         @Override
clampViewPositionVertical(View child, int top, int dy)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