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
17package com.android.printspooler.widget;
18
19import android.content.Context;
20import android.support.v4.widget.ViewDragHelper;
21import android.util.AttributeSet;
22import android.view.MotionEvent;
23import android.view.View;
24import android.view.ViewGroup;
25import android.view.inputmethod.InputMethodManager;
26import 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")
38public 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.getResources().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 = (ViewGroup) 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 = (ViewGroup) 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        public void onViewReleased(View child, float velocityX, float velocityY) {
419            final int childTop = child.getTop();
420
421            final int openedOptionsY = getOpenedOptionsY();
422            final int closedOptionsY = getClosedOptionsY();
423
424            if (childTop == openedOptionsY || childTop == closedOptionsY) {
425                return;
426            }
427
428            final int halfRange = closedOptionsY + (openedOptionsY - closedOptionsY) / 2;
429            if (childTop < halfRange) {
430                mDragger.smoothSlideViewTo(child, child.getLeft(), closedOptionsY);
431            } else {
432                mDragger.smoothSlideViewTo(child, child.getLeft(), openedOptionsY);
433            }
434
435            invalidate();
436        }
437
438        public int getOrderedChildIndex(int index) {
439            return getChildCount() - index - 1;
440        }
441
442        public int getViewVerticalDragRange(View child) {
443            return mDraggableContent.getHeight();
444        }
445
446        public int clampViewPositionVertical(View child, int top, int dy) {
447            final int staticOptionBottom = mStaticContent.getBottom();
448            return Math.max(Math.min(top, getOpenedOptionsY()), getClosedOptionsY());
449        }
450    }
451}
452