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