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