/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.printspooler.widget; import android.content.Context; import android.support.v4.widget.ViewDragHelper; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import com.android.printspooler.R; /** * This class is a layout manager for the print screen. It has a sliding * area that contains the print options. If the sliding area is open the * print options are visible and if it is closed a summary of the print * job is shown. Under the sliding area there is a place for putting * arbitrary content such as preview, error message, progress indicator, * etc. The sliding area is covering the content holder under it when * the former is opened. */ @SuppressWarnings("unused") public final class PrintContentView extends ViewGroup implements View.OnClickListener { private static final int FIRST_POINTER_ID = 0; private static final int ALPHA_MASK = 0xff000000; private static final int ALPHA_SHIFT = 24; private static final int COLOR_MASK = 0xffffff; private final ViewDragHelper mDragger; private final int mScrimColor; private View mStaticContent; private ViewGroup mSummaryContent; private View mDynamicContent; private View mDraggableContent; private View mPrintButton; private View mMoreOptionsButton; private ViewGroup mOptionsContainer; private View mEmbeddedContentContainer; private View mEmbeddedContentScrim; private View mExpandCollapseHandle; private View mExpandCollapseIcon; private int mClosedOptionsOffsetY; private int mCurrentOptionsOffsetY = Integer.MIN_VALUE; private OptionsStateChangeListener mOptionsStateChangeListener; private OptionsStateController mOptionsStateController; private int mOldDraggableHeight; private float mDragProgress; public interface OptionsStateChangeListener { public void onOptionsOpened(); public void onOptionsClosed(); } public interface OptionsStateController { public boolean canOpenOptions(); public boolean canCloseOptions(); } public PrintContentView(Context context, AttributeSet attrs) { super(context, attrs); mDragger = ViewDragHelper.create(this, new DragCallbacks()); mScrimColor = context.getColor(R.color.print_preview_scrim_color); // The options view is sliding under the static header but appears // after it in the layout, so we will draw in opposite order. setChildrenDrawingOrderEnabled(true); } public void setOptionsStateChangeListener(OptionsStateChangeListener listener) { mOptionsStateChangeListener = listener; } public void setOpenOptionsController(OptionsStateController controller) { mOptionsStateController = controller; } public boolean isOptionsOpened() { return mCurrentOptionsOffsetY == 0; } private boolean isOptionsClosed() { return mCurrentOptionsOffsetY == mClosedOptionsOffsetY; } public void openOptions() { if (isOptionsOpened()) { return; } mDragger.smoothSlideViewTo(mDynamicContent, mDynamicContent.getLeft(), getOpenedOptionsY()); invalidate(); } public void closeOptions() { if (isOptionsClosed()) { return; } mDragger.smoothSlideViewTo(mDynamicContent, mDynamicContent.getLeft(), getClosedOptionsY()); invalidate(); } @Override protected int getChildDrawingOrder(int childCount, int i) { return childCount - i - 1; } @Override protected void onFinishInflate() { mStaticContent = findViewById(R.id.static_content); mSummaryContent = (ViewGroup) findViewById(R.id.summary_content); mDynamicContent = findViewById(R.id.dynamic_content); mDraggableContent = findViewById(R.id.draggable_content); mPrintButton = findViewById(R.id.print_button); mMoreOptionsButton = findViewById(R.id.more_options_button); mOptionsContainer = (ViewGroup) findViewById(R.id.options_container); mEmbeddedContentContainer = findViewById(R.id.embedded_content_container); mEmbeddedContentScrim = findViewById(R.id.embedded_content_scrim); mExpandCollapseHandle = findViewById(R.id.expand_collapse_handle); mExpandCollapseIcon = findViewById(R.id.expand_collapse_icon); mExpandCollapseHandle.setOnClickListener(this); mSummaryContent.setOnClickListener(this); // Make sure we start in a closed options state. onDragProgress(1.0f); // The framework gives focus to the frist focusable and we // do not want that, hence we will take focus instead. setFocusableInTouchMode(true); } @Override public void focusableViewAvailable(View v) { // The framework gives focus to the frist focusable and we // do not want that, hence do not announce new focusables. return; } @Override public void onClick(View view) { if (view == mExpandCollapseHandle || view == mSummaryContent) { if (isOptionsClosed() && mOptionsStateController.canOpenOptions()) { openOptions(); } else if (isOptionsOpened() && mOptionsStateController.canCloseOptions()) { closeOptions(); } // else in open/close progress do nothing. } else if (view == mEmbeddedContentScrim) { if (isOptionsOpened() && mOptionsStateController.canCloseOptions()) { closeOptions(); } } } @Override public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { /* do nothing */ } @Override public boolean onTouchEvent(MotionEvent event) { mDragger.processTouchEvent(event); return true; } @Override public boolean onInterceptTouchEvent(MotionEvent event) { return mDragger.shouldInterceptTouchEvent(event) || super.onInterceptTouchEvent(event); } @Override public void computeScroll() { if (mDragger.continueSettling(true)) { postInvalidateOnAnimation(); } } private int computeScrimColor() { final int baseAlpha = (mScrimColor & ALPHA_MASK) >>> ALPHA_SHIFT; final int adjustedAlpha = (int) (baseAlpha * (1 - mDragProgress)); return adjustedAlpha << ALPHA_SHIFT | (mScrimColor & COLOR_MASK); } private int getOpenedOptionsY() { return mStaticContent.getBottom(); } private int getClosedOptionsY() { return getOpenedOptionsY() + mClosedOptionsOffsetY; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final boolean wasOpened = isOptionsOpened(); measureChild(mStaticContent, widthMeasureSpec, heightMeasureSpec); if (mSummaryContent.getVisibility() != View.GONE) { measureChild(mSummaryContent, widthMeasureSpec, heightMeasureSpec); } measureChild(mDynamicContent, widthMeasureSpec, heightMeasureSpec); measureChild(mPrintButton, widthMeasureSpec, heightMeasureSpec); // The height of the draggable content may change and if that happens // we have to adjust the sliding area closed state offset. mClosedOptionsOffsetY = mSummaryContent.getMeasuredHeight() - mDraggableContent.getMeasuredHeight(); if (mCurrentOptionsOffsetY == Integer.MIN_VALUE) { mCurrentOptionsOffsetY = mClosedOptionsOffsetY; } final int heightSize = MeasureSpec.getSize(heightMeasureSpec); // The content host must be maximally large size that fits entirely // on the screen when the options are collapsed. ViewGroup.LayoutParams params = mEmbeddedContentContainer.getLayoutParams(); params.height = heightSize - mStaticContent.getMeasuredHeight() - mSummaryContent.getMeasuredHeight() - mDynamicContent.getMeasuredHeight() + mDraggableContent.getMeasuredHeight(); // The height of the draggable content may change and if that happens // we have to adjust the current offset to ensure the sliding area is // at the correct position. if (mOldDraggableHeight != mDraggableContent.getMeasuredHeight()) { if (mOldDraggableHeight != 0) { mCurrentOptionsOffsetY = wasOpened ? 0 : mClosedOptionsOffsetY; } mOldDraggableHeight = mDraggableContent.getMeasuredHeight(); } // The content host can grow vertically as much as needed - we will be covering it. final int hostHeightMeasureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.UNSPECIFIED, 0); measureChild(mEmbeddedContentContainer, widthMeasureSpec, hostHeightMeasureSpec); setMeasuredDimension(resolveSize(MeasureSpec.getSize(widthMeasureSpec), widthMeasureSpec), resolveSize(heightSize, heightMeasureSpec)); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { mStaticContent.layout(left, top, right, mStaticContent.getMeasuredHeight()); if (mSummaryContent.getVisibility() != View.GONE) { mSummaryContent.layout(left, mStaticContent.getMeasuredHeight(), right, mStaticContent.getMeasuredHeight() + mSummaryContent.getMeasuredHeight()); } final int dynContentTop = mStaticContent.getMeasuredHeight() + mCurrentOptionsOffsetY; final int dynContentBottom = dynContentTop + mDynamicContent.getMeasuredHeight(); mDynamicContent.layout(left, dynContentTop, right, dynContentBottom); MarginLayoutParams params = (MarginLayoutParams) mPrintButton.getLayoutParams(); final int printButtonLeft; if (getLayoutDirection() == View.LAYOUT_DIRECTION_LTR) { printButtonLeft = right - mPrintButton.getMeasuredWidth() - params.getMarginStart(); } else { printButtonLeft = left + params.getMarginStart(); } final int printButtonTop = dynContentBottom - mPrintButton.getMeasuredHeight() / 2; final int printButtonRight = printButtonLeft + mPrintButton.getMeasuredWidth(); final int printButtonBottom = printButtonTop + mPrintButton.getMeasuredHeight(); mPrintButton.layout(printButtonLeft, printButtonTop, printButtonRight, printButtonBottom); final int embContentTop = mStaticContent.getMeasuredHeight() + mClosedOptionsOffsetY + mDynamicContent.getMeasuredHeight(); final int embContentBottom = embContentTop + mEmbeddedContentContainer.getMeasuredHeight(); mEmbeddedContentContainer.layout(left, embContentTop, right, embContentBottom); } @Override public LayoutParams generateLayoutParams(AttributeSet attrs) { return new ViewGroup.MarginLayoutParams(getContext(), attrs); } private void onDragProgress(float progress) { if (Float.compare(mDragProgress, progress) == 0) { return; } if ((mDragProgress == 0 && progress > 0) || (mDragProgress == 1.0f && progress < 1.0f)) { mSummaryContent.setLayerType(View.LAYER_TYPE_HARDWARE, null); mDraggableContent.setLayerType(View.LAYER_TYPE_HARDWARE, null); mMoreOptionsButton.setLayerType(View.LAYER_TYPE_HARDWARE, null); ensureImeClosedAndInputFocusCleared(); } if ((mDragProgress > 0 && progress == 0) || (mDragProgress < 1.0f && progress == 1.0f)) { mSummaryContent.setLayerType(View.LAYER_TYPE_NONE, null); mDraggableContent.setLayerType(View.LAYER_TYPE_NONE, null); mMoreOptionsButton.setLayerType(View.LAYER_TYPE_NONE, null); mMoreOptionsButton.setLayerType(View.LAYER_TYPE_NONE, null); } mDragProgress = progress; mSummaryContent.setAlpha(progress); final float inverseAlpha = 1.0f - progress; mOptionsContainer.setAlpha(inverseAlpha); mMoreOptionsButton.setAlpha(inverseAlpha); mEmbeddedContentScrim.setBackgroundColor(computeScrimColor()); if (progress == 0) { if (mOptionsStateChangeListener != null) { mOptionsStateChangeListener.onOptionsOpened(); } mExpandCollapseHandle.setContentDescription( mContext.getString(R.string.collapse_handle)); announceForAccessibility(mContext.getString(R.string.print_options_expanded)); mSummaryContent.setVisibility(View.GONE); mEmbeddedContentScrim.setOnClickListener(this); mExpandCollapseIcon.setBackgroundResource(R.drawable.ic_expand_less); } else { mSummaryContent.setVisibility(View.VISIBLE); } if (progress == 1.0f) { if (mOptionsStateChangeListener != null) { mOptionsStateChangeListener.onOptionsClosed(); } mExpandCollapseHandle.setContentDescription( mContext.getString(R.string.expand_handle)); announceForAccessibility(mContext.getString(R.string.print_options_collapsed)); if (mMoreOptionsButton.getVisibility() != View.GONE) { mMoreOptionsButton.setVisibility(View.INVISIBLE); } mDraggableContent.setVisibility(View.INVISIBLE); // If we change the scrim visibility the dimming is lagging // and is janky. Now it is there but transparent, doing nothing. mEmbeddedContentScrim.setOnClickListener(null); mEmbeddedContentScrim.setClickable(false); mExpandCollapseIcon.setBackgroundResource(R.drawable.ic_expand_more); } else { if (mMoreOptionsButton.getVisibility() != View.GONE) { mMoreOptionsButton.setVisibility(View.VISIBLE); } mDraggableContent.setVisibility(View.VISIBLE); } } private void ensureImeClosedAndInputFocusCleared() { View focused = findFocus(); if (focused != null && focused.isFocused()) { InputMethodManager imm = (InputMethodManager) mContext.getSystemService( Context.INPUT_METHOD_SERVICE); if (imm.isActive(focused)) { imm.hideSoftInputFromWindow(getWindowToken(), 0); } focused.clearFocus(); } } private final class DragCallbacks extends ViewDragHelper.Callback { @Override public boolean tryCaptureView(View child, int pointerId) { if (isOptionsOpened() && !mOptionsStateController.canCloseOptions() || isOptionsClosed() && !mOptionsStateController.canOpenOptions()) { return false; } return child == mDynamicContent && pointerId == FIRST_POINTER_ID; } @Override public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { if ((isOptionsClosed() || isOptionsClosed()) && dy <= 0) { return; } mCurrentOptionsOffsetY += dy; final float progress = ((float) top - getOpenedOptionsY()) / (getClosedOptionsY() - getOpenedOptionsY()); mPrintButton.offsetTopAndBottom(dy); mDraggableContent.notifySubtreeAccessibilityStateChangedIfNeeded(); onDragProgress(progress); } @Override public void onViewReleased(View child, float velocityX, float velocityY) { final int childTop = child.getTop(); final int openedOptionsY = getOpenedOptionsY(); final int closedOptionsY = getClosedOptionsY(); if (childTop == openedOptionsY || childTop == closedOptionsY) { return; } final int halfRange = closedOptionsY + (openedOptionsY - closedOptionsY) / 2; if (childTop < halfRange) { mDragger.smoothSlideViewTo(child, child.getLeft(), closedOptionsY); } else { mDragger.smoothSlideViewTo(child, child.getLeft(), openedOptionsY); } invalidate(); } @Override public int getOrderedChildIndex(int index) { return getChildCount() - index - 1; } @Override public int getViewVerticalDragRange(View child) { return mDraggableContent.getHeight(); } @Override public int clampViewPositionVertical(View child, int top, int dy) { final int staticOptionBottom = mStaticContent.getBottom(); return Math.max(Math.min(top, getOpenedOptionsY()), getClosedOptionsY()); } } }