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