2 * Copyright (C) 2014 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.printspooler.widget;
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;
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.
37 @SuppressWarnings("unused")
38 public final class PrintContentView extends ViewGroup implements View.OnClickListener {
39 private static final int FIRST_POINTER_ID = 0;
41 private static final int ALPHA_MASK = 0xff000000;
42 private static final int ALPHA_SHIFT = 24;
44 private static final int COLOR_MASK = 0xffffff;
46 private final ViewDragHelper mDragger;
48 private final int mScrimColor;
50 private View mStaticContent;
51 private ViewGroup mSummaryContent;
52 private View mDynamicContent;
54 private View mDraggableContent;
55 private View mPrintButton;
56 private View mMoreOptionsButton;
57 private ViewGroup mOptionsContainer;
59 private View mEmbeddedContentContainer;
60 private View mEmbeddedContentScrim;
62 private View mExpandCollapseHandle;
63 private View mExpandCollapseIcon;
65 private int mClosedOptionsOffsetY;
66 private int mCurrentOptionsOffsetY = Integer.MIN_VALUE;
68 private OptionsStateChangeListener mOptionsStateChangeListener;
70 private OptionsStateController mOptionsStateController;
72 private int mOldDraggableHeight;
74 private float mDragProgress;
76 public interface OptionsStateChangeListener {
77 public void onOptionsOpened();
78 public void onOptionsClosed();
81 public interface OptionsStateController {
82 public boolean canOpenOptions();
83 public boolean canCloseOptions();
86 public PrintContentView(Context context, AttributeSet attrs) {
87 super(context, attrs);
88 mDragger = ViewDragHelper.create(this, new DragCallbacks());
90 mScrimColor = context.getColor(R.color.print_preview_scrim_color);
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);
97 public void setOptionsStateChangeListener(OptionsStateChangeListener listener) {
98 mOptionsStateChangeListener = listener;
101 public void setOpenOptionsController(OptionsStateController controller) {
102 mOptionsStateController = controller;
105 public boolean isOptionsOpened() {
106 return mCurrentOptionsOffsetY == 0;
109 private boolean isOptionsClosed() {
110 return mCurrentOptionsOffsetY == mClosedOptionsOffsetY;
113 public void openOptions() {
114 if (isOptionsOpened()) {
117 mDragger.smoothSlideViewTo(mDynamicContent, mDynamicContent.getLeft(),
118 getOpenedOptionsY());
122 public void closeOptions() {
123 if (isOptionsClosed()) {
126 mDragger.smoothSlideViewTo(mDynamicContent, mDynamicContent.getLeft(),
127 getClosedOptionsY());
132 protected int getChildDrawingOrder(int childCount, int i) {
133 return childCount - i - 1;
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);
150 mExpandCollapseHandle.setOnClickListener(this);
151 mSummaryContent.setOnClickListener(this);
153 // Make sure we start in a closed options state.
154 onDragProgress(1.0f);
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);
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.
169 public void onClick(View view) {
170 if (view == mExpandCollapseHandle || view == mSummaryContent) {
171 if (isOptionsClosed() && mOptionsStateController.canOpenOptions()) {
173 } else if (isOptionsOpened() && mOptionsStateController.canCloseOptions()) {
175 } // else in open/close progress do nothing.
176 } else if (view == mEmbeddedContentScrim) {
177 if (isOptionsOpened() && mOptionsStateController.canCloseOptions()) {
184 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
189 public boolean onTouchEvent(MotionEvent event) {
190 mDragger.processTouchEvent(event);
195 public boolean onInterceptTouchEvent(MotionEvent event) {
196 return mDragger.shouldInterceptTouchEvent(event)
197 || super.onInterceptTouchEvent(event);
201 public void computeScroll() {
202 if (mDragger.continueSettling(true)) {
203 postInvalidateOnAnimation();
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);
213 private int getOpenedOptionsY() {
214 return mStaticContent.getBottom();
217 private int getClosedOptionsY() {
218 return getOpenedOptionsY() + mClosedOptionsOffsetY;
222 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
223 final boolean wasOpened = isOptionsOpened();
225 measureChild(mStaticContent, widthMeasureSpec, heightMeasureSpec);
227 if (mSummaryContent.getVisibility() != View.GONE) {
228 measureChild(mSummaryContent, widthMeasureSpec, heightMeasureSpec);
231 measureChild(mDynamicContent, widthMeasureSpec, heightMeasureSpec);
233 measureChild(mPrintButton, widthMeasureSpec, heightMeasureSpec);
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();
240 if (mCurrentOptionsOffsetY == Integer.MIN_VALUE) {
241 mCurrentOptionsOffsetY = mClosedOptionsOffsetY;
244 final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
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();
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;
260 mOldDraggableHeight = mDraggableContent.getMeasuredHeight();
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);
267 setMeasuredDimension(resolveSize(MeasureSpec.getSize(widthMeasureSpec), widthMeasureSpec),
268 resolveSize(heightSize, heightMeasureSpec));
272 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
273 mStaticContent.layout(left, top, right, mStaticContent.getMeasuredHeight());
275 if (mSummaryContent.getVisibility() != View.GONE) {
276 mSummaryContent.layout(left, mStaticContent.getMeasuredHeight(), right,
277 mStaticContent.getMeasuredHeight() + mSummaryContent.getMeasuredHeight());
280 final int dynContentTop = mStaticContent.getMeasuredHeight() + mCurrentOptionsOffsetY;
281 final int dynContentBottom = dynContentTop + mDynamicContent.getMeasuredHeight();
283 mDynamicContent.layout(left, dynContentTop, right, dynContentBottom);
285 MarginLayoutParams params = (MarginLayoutParams) mPrintButton.getLayoutParams();
287 final int printButtonLeft;
288 if (getLayoutDirection() == View.LAYOUT_DIRECTION_LTR) {
289 printButtonLeft = right - mPrintButton.getMeasuredWidth() - params.getMarginStart();
291 printButtonLeft = left + params.getMarginStart();
293 final int printButtonTop = dynContentBottom - mPrintButton.getMeasuredHeight() / 2;
294 final int printButtonRight = printButtonLeft + mPrintButton.getMeasuredWidth();
295 final int printButtonBottom = printButtonTop + mPrintButton.getMeasuredHeight();
297 mPrintButton.layout(printButtonLeft, printButtonTop, printButtonRight, printButtonBottom);
299 final int embContentTop = mStaticContent.getMeasuredHeight() + mClosedOptionsOffsetY
300 + mDynamicContent.getMeasuredHeight();
301 final int embContentBottom = embContentTop + mEmbeddedContentContainer.getMeasuredHeight();
303 mEmbeddedContentContainer.layout(left, embContentTop, right, embContentBottom);
307 public LayoutParams generateLayoutParams(AttributeSet attrs) {
308 return new ViewGroup.MarginLayoutParams(getContext(), attrs);
311 private void onDragProgress(float progress) {
312 if (Float.compare(mDragProgress, progress) == 0) {
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();
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);
331 mDragProgress = progress;
333 mSummaryContent.setAlpha(progress);
335 final float inverseAlpha = 1.0f - progress;
336 mOptionsContainer.setAlpha(inverseAlpha);
337 mMoreOptionsButton.setAlpha(inverseAlpha);
339 mEmbeddedContentScrim.setBackgroundColor(computeScrimColor());
341 if (mOptionsStateChangeListener != null) {
342 mOptionsStateChangeListener.onOptionsOpened();
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);
351 mSummaryContent.setVisibility(View.VISIBLE);
354 if (progress == 1.0f) {
355 if (mOptionsStateChangeListener != null) {
356 mOptionsStateChangeListener.onOptionsClosed();
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);
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);
371 if (mMoreOptionsButton.getVisibility() != View.GONE) {
372 mMoreOptionsButton.setVisibility(View.VISIBLE);
374 mDraggableContent.setVisibility(View.VISIBLE);
378 private void ensureImeClosedAndInputFocusCleared() {
379 View focused = findFocus();
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);
387 focused.clearFocus();
391 private final class DragCallbacks extends ViewDragHelper.Callback {
393 public boolean tryCaptureView(View child, int pointerId) {
394 if (isOptionsOpened() && !mOptionsStateController.canCloseOptions()
395 || isOptionsClosed() && !mOptionsStateController.canOpenOptions()) {
398 return child == mDynamicContent && pointerId == FIRST_POINTER_ID;
402 public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
403 if ((isOptionsClosed() || isOptionsClosed()) && dy <= 0) {
407 mCurrentOptionsOffsetY += dy;
408 final float progress = ((float) top - getOpenedOptionsY())
409 / (getClosedOptionsY() - getOpenedOptionsY());
411 mPrintButton.offsetTopAndBottom(dy);
413 mDraggableContent.notifySubtreeAccessibilityStateChangedIfNeeded();
415 onDragProgress(progress);
419 public void onViewReleased(View child, float velocityX, float velocityY) {
420 final int childTop = child.getTop();
422 final int openedOptionsY = getOpenedOptionsY();
423 final int closedOptionsY = getClosedOptionsY();
425 if (childTop == openedOptionsY || childTop == closedOptionsY) {
429 final int halfRange = closedOptionsY + (openedOptionsY - closedOptionsY) / 2;
430 if (childTop < halfRange) {
431 mDragger.smoothSlideViewTo(child, child.getLeft(), closedOptionsY);
433 mDragger.smoothSlideViewTo(child, child.getLeft(), openedOptionsY);
440 public int getOrderedChildIndex(int index) {
441 return getChildCount() - index - 1;
445 public int getViewVerticalDragRange(View child) {
446 return mDraggableContent.getHeight();
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());