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