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