| /* |
| * 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.systemui.recents.views; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.ObjectAnimator; |
| import android.animation.ValueAnimator; |
| import android.content.Context; |
| import android.graphics.*; |
| import android.graphics.Bitmap.Config; |
| import android.util.AttributeSet; |
| import android.view.accessibility.AccessibilityManager; |
| import android.view.View; |
| import android.view.ViewOutlineProvider; |
| import android.view.animation.AccelerateInterpolator; |
| import android.view.animation.DecelerateInterpolator; |
| import android.view.animation.Interpolator; |
| import android.widget.FrameLayout; |
| |
| import com.android.internal.logging.MetricsLogger; |
| import com.android.systemui.R; |
| import com.android.systemui.recents.Constants; |
| import com.android.systemui.recents.RecentsConfiguration; |
| import com.android.systemui.recents.misc.Utilities; |
| import com.android.systemui.recents.model.Task; |
| import com.android.systemui.statusbar.phone.PhoneStatusBar; |
| |
| /* A task view */ |
| public class TaskView extends FrameLayout implements Task.TaskCallbacks, |
| View.OnClickListener, View.OnLongClickListener { |
| |
| /** The TaskView callbacks */ |
| interface TaskViewCallbacks { |
| public void onTaskViewAppIconClicked(TaskView tv); |
| public void onTaskViewAppInfoClicked(TaskView tv); |
| public void onTaskViewClicked(TaskView tv, Task task, boolean lockToTask); |
| public void onTaskViewDismissed(TaskView tv); |
| public void onTaskViewClipStateChanged(TaskView tv); |
| public void onTaskViewFocusChanged(TaskView tv, boolean focused); |
| public void onTaskResize(TaskView tv); |
| } |
| |
| RecentsConfiguration mConfig; |
| |
| float mTaskProgress; |
| ObjectAnimator mTaskProgressAnimator; |
| float mMaxDimScale; |
| int mDimAlpha; |
| AccelerateInterpolator mDimInterpolator = new AccelerateInterpolator(1f); |
| PorterDuffColorFilter mDimColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_ATOP); |
| Paint mDimLayerPaint = new Paint(); |
| float mActionButtonTranslationZ; |
| |
| Task mTask; |
| boolean mTaskDataLoaded; |
| boolean mIsFocused; |
| boolean mFocusAnimationsEnabled; |
| boolean mClipViewInStack; |
| AnimateableViewBounds mViewBounds; |
| |
| View mContent; |
| TaskViewThumbnail mThumbnailView; |
| TaskViewHeader mHeaderView; |
| View mActionButtonView; |
| TaskViewCallbacks mCb; |
| |
| // Focus animation |
| float mFocusProgress; |
| Paint mFocusPaint = new Paint(); |
| int mFocusColor = 0xff009688; |
| int mFocusAlpha = 0x40; |
| int mFocusBorderAlpha; |
| static Interpolator sFocusInInterpolator = new DecelerateInterpolator(3f); |
| static Interpolator sFocusInRadiusInterpolator = new DecelerateInterpolator(); |
| static Interpolator sFocusOutInterpolator = new DecelerateInterpolator(); |
| ObjectAnimator mFocusAnimator; |
| static long sFocusInDurationMs = 350; |
| static long sFocusOutDurationMs = 200; |
| float mFocusInCircleRadiusProgress; |
| int mFocusInFillAlpha; |
| int mFocusInCircleAlpha; |
| int mFocusOutFillAlpha; |
| boolean mFocusAnimatorWasTriggered; |
| int mFocusBorderSize; |
| |
| // Optimizations |
| ValueAnimator.AnimatorUpdateListener mUpdateDimListener = |
| new ValueAnimator.AnimatorUpdateListener() { |
| @Override |
| public void onAnimationUpdate(ValueAnimator animation) { |
| setTaskProgress((Float) animation.getAnimatedValue()); |
| } |
| }; |
| |
| public TaskView(Context context) { |
| this(context, null); |
| } |
| |
| public TaskView(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public TaskView(Context context, AttributeSet attrs, int defStyleAttr) { |
| this(context, attrs, defStyleAttr, 0); |
| } |
| |
| public TaskView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { |
| super(context, attrs, defStyleAttr, defStyleRes); |
| mConfig = RecentsConfiguration.getInstance(); |
| mMaxDimScale = mConfig.taskStackMaxDim / 255f; |
| mClipViewInStack = true; |
| mViewBounds = new AnimateableViewBounds(this, mConfig.taskViewRoundedCornerRadiusPx); |
| setTaskProgress(getTaskProgress()); |
| setDim(getDim()); |
| if (mConfig.fakeShadows) { |
| setBackground(new FakeShadowDrawable(context.getResources(), mConfig)); |
| } |
| setOutlineProvider(mViewBounds); |
| |
| mFocusAnimator = ObjectAnimator.ofFloat(this, "focusProgress", 1f); |
| mFocusAnimator.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationStart(Animator animator) { |
| mFocusAnimatorWasTriggered = true; |
| } |
| }); |
| |
| mFocusColor = |
| context.getResources().getColor(R.color.recents_focus_color); |
| mFocusPaint.setColor(mFocusColor); |
| mFocusBorderSize = |
| context.getResources().getDimensionPixelSize(R.dimen.recents_border_size); |
| } |
| |
| /** Set callback */ |
| void setCallbacks(TaskViewCallbacks cb) { |
| mCb = cb; |
| } |
| |
| /** Resets this TaskView for reuse. */ |
| void reset() { |
| resetViewProperties(); |
| setClipViewInStack(false); |
| setCallbacks(null); |
| disableFocusAnimations(); |
| } |
| |
| /** Gets the task */ |
| Task getTask() { |
| return mTask; |
| } |
| |
| /** Returns the view bounds. */ |
| AnimateableViewBounds getViewBounds() { |
| return mViewBounds; |
| } |
| |
| @Override |
| protected void onFinishInflate() { |
| // Bind the views |
| mContent = findViewById(R.id.task_view_content); |
| mHeaderView = (TaskViewHeader) findViewById(R.id.task_view_bar); |
| mThumbnailView = (TaskViewThumbnail) findViewById(R.id.task_view_thumbnail); |
| mThumbnailView.updateClipToTaskBar(mHeaderView); |
| mActionButtonView = findViewById(R.id.lock_to_app_fab); |
| mActionButtonView.setOutlineProvider(new ViewOutlineProvider() { |
| @Override |
| public void getOutline(View view, Outline outline) { |
| // Set the outline to match the FAB background |
| outline.setOval(0, 0, mActionButtonView.getWidth(), mActionButtonView.getHeight()); |
| } |
| }); |
| mActionButtonTranslationZ = mActionButtonView.getTranslationZ(); |
| } |
| |
| @Override |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| int width = MeasureSpec.getSize(widthMeasureSpec); |
| int height = MeasureSpec.getSize(heightMeasureSpec); |
| |
| int widthWithoutPadding = width - mPaddingLeft - mPaddingRight; |
| int heightWithoutPadding = height - mPaddingTop - mPaddingBottom; |
| |
| // Measure the content |
| mContent.measure(MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY), |
| MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY)); |
| |
| // Measure the bar view, and action button |
| mHeaderView.measure(MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY), |
| MeasureSpec.makeMeasureSpec(mConfig.taskBarHeight, MeasureSpec.EXACTLY)); |
| mActionButtonView.measure( |
| MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.AT_MOST), |
| MeasureSpec.makeMeasureSpec(heightWithoutPadding, MeasureSpec.AT_MOST)); |
| // Measure the thumbnail to be square |
| mThumbnailView.measure( |
| MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY), |
| MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY)); |
| setMeasuredDimension(width, height); |
| invalidateOutline(); |
| } |
| |
| /** Synchronizes this view's properties with the task's transform */ |
| void updateViewPropertiesToTaskTransform(TaskViewTransform toTransform, int duration) { |
| updateViewPropertiesToTaskTransform(toTransform, duration, null); |
| } |
| |
| void updateViewPropertiesToTaskTransform(TaskViewTransform toTransform, int duration, |
| ValueAnimator.AnimatorUpdateListener updateCallback) { |
| // Apply the transform |
| toTransform.applyToTaskView(this, duration, mConfig.fastOutSlowInInterpolator, false, |
| !mConfig.fakeShadows, updateCallback); |
| |
| // Update the task progress |
| Utilities.cancelAnimationWithoutCallbacks(mTaskProgressAnimator); |
| if (duration <= 0) { |
| setTaskProgress(toTransform.p); |
| } else { |
| mTaskProgressAnimator = ObjectAnimator.ofFloat(this, "taskProgress", toTransform.p); |
| mTaskProgressAnimator.setDuration(duration); |
| mTaskProgressAnimator.addUpdateListener(mUpdateDimListener); |
| mTaskProgressAnimator.start(); |
| } |
| } |
| |
| /** Resets this view's properties */ |
| void resetViewProperties() { |
| unsetFocusedTask(); |
| setDim(0); |
| setLayerType(View.LAYER_TYPE_NONE, null); |
| TaskViewTransform.reset(this); |
| if (mActionButtonView != null) { |
| mActionButtonView.setScaleX(1f); |
| mActionButtonView.setScaleY(1f); |
| mActionButtonView.setAlpha(1f); |
| mActionButtonView.setTranslationZ(mActionButtonTranslationZ); |
| } |
| } |
| |
| /** |
| * When we are un/filtering, this method will set up the transform that we are animating to, |
| * in order to hide the task. |
| */ |
| void prepareTaskTransformForFilterTaskHidden(TaskViewTransform toTransform) { |
| // Fade the view out and slide it away |
| toTransform.alpha = 0f; |
| toTransform.translationY += 200; |
| toTransform.translationZ = 0; |
| } |
| |
| /** |
| * When we are un/filtering, this method will setup the transform that we are animating from, |
| * in order to show the task. |
| */ |
| void prepareTaskTransformForFilterTaskVisible(TaskViewTransform fromTransform) { |
| // Fade the view in |
| fromTransform.alpha = 0f; |
| } |
| |
| /** |
| * Prepares this task view for the enter-recents animations. This is called earlier in the |
| * first layout because the actual animation into recents may take a long time. |
| */ |
| void prepareEnterRecentsAnimation(boolean isTaskViewLaunchTargetTask, |
| boolean occludesLaunchTarget, int offscreenY) { |
| int initialDim = getDim(); |
| if (mConfig.launchedHasConfigurationChanged) { |
| // Just load the views as-is |
| } else if (mConfig.launchedFromAppWithThumbnail) { |
| if (isTaskViewLaunchTargetTask) { |
| // Set the dim to 0 so we can animate it in |
| initialDim = 0; |
| // Hide the action button |
| mActionButtonView.setAlpha(0f); |
| } else if (occludesLaunchTarget) { |
| // Move the task view off screen (below) so we can animate it in |
| setTranslationY(offscreenY); |
| } |
| |
| } else if (mConfig.launchedFromHome) { |
| // Move the task view off screen (below) so we can animate it in |
| setTranslationY(offscreenY); |
| setTranslationZ(0); |
| setScaleX(1f); |
| setScaleY(1f); |
| } |
| // Apply the current dim |
| setDim(initialDim); |
| // Prepare the thumbnail view alpha |
| mThumbnailView.prepareEnterRecentsAnimation(isTaskViewLaunchTargetTask); |
| } |
| |
| /** Animates this task view as it enters recents */ |
| void startEnterRecentsAnimation(final ViewAnimation.TaskViewEnterContext ctx) { |
| final TaskViewTransform transform = ctx.currentTaskTransform; |
| int startDelay = 0; |
| |
| if (mConfig.launchedFromAppWithThumbnail) { |
| if (mTask.isLaunchTarget) { |
| // Animate the dim/overlay |
| if (Constants.DebugFlags.App.EnableThumbnailAlphaOnFrontmost) { |
| // Animate the thumbnail alpha before the dim animation (to prevent updating the |
| // hardware layer) |
| mThumbnailView.startEnterRecentsAnimation(mConfig.transitionEnterFromAppDelay, |
| new Runnable() { |
| @Override |
| public void run() { |
| animateDimToProgress(0, mConfig.taskViewEnterFromAppDuration, |
| ctx.postAnimationTrigger.decrementOnAnimationEnd()); |
| } |
| }); |
| } else { |
| // Immediately start the dim animation |
| animateDimToProgress(mConfig.transitionEnterFromAppDelay, |
| mConfig.taskViewEnterFromAppDuration, |
| ctx.postAnimationTrigger.decrementOnAnimationEnd()); |
| } |
| ctx.postAnimationTrigger.increment(); |
| |
| // Animate the action button in |
| fadeInActionButton(mConfig.transitionEnterFromAppDelay, |
| mConfig.taskViewEnterFromAppDuration); |
| } else { |
| // Animate the task up if it was occluding the launch target |
| if (ctx.currentTaskOccludesLaunchTarget) { |
| setTranslationY(transform.translationY |
| + mConfig.taskViewAffiliateGroupEnterOffsetPx); |
| setAlpha(0f); |
| animate().alpha(1f) |
| .translationY(transform.translationY) |
| .setStartDelay(mConfig.transitionEnterFromAppDelay) |
| .setUpdateListener(null) |
| .setInterpolator(mConfig.fastOutSlowInInterpolator) |
| .setDuration(mConfig.taskViewEnterFromHomeDuration) |
| .withEndAction(new Runnable() { |
| @Override |
| public void run() { |
| // Decrement the post animation trigger |
| ctx.postAnimationTrigger.decrement(); |
| } |
| }) |
| .start(); |
| ctx.postAnimationTrigger.increment(); |
| } |
| } |
| startDelay = mConfig.transitionEnterFromAppDelay; |
| |
| } else if (mConfig.launchedFromHome) { |
| // Animate the tasks up |
| int frontIndex = (ctx.currentStackViewCount - ctx.currentStackViewIndex - 1); |
| int delay = mConfig.transitionEnterFromHomeDelay + |
| frontIndex * mConfig.taskViewEnterFromHomeStaggerDelay; |
| |
| setScaleX(transform.scale); |
| setScaleY(transform.scale); |
| if (!mConfig.fakeShadows) { |
| animate().translationZ(transform.translationZ); |
| } |
| animate() |
| .translationY(transform.translationY) |
| .setStartDelay(delay) |
| .setUpdateListener(ctx.updateListener) |
| .setInterpolator(mConfig.quintOutInterpolator) |
| .setDuration(mConfig.taskViewEnterFromHomeDuration + |
| frontIndex * mConfig.taskViewEnterFromHomeStaggerDelay) |
| .withEndAction(new Runnable() { |
| @Override |
| public void run() { |
| // Decrement the post animation trigger |
| ctx.postAnimationTrigger.decrement(); |
| } |
| }) |
| .start(); |
| ctx.postAnimationTrigger.increment(); |
| startDelay = delay; |
| } |
| |
| // Enable the focus animations from this point onwards so that they aren't affected by the |
| // window transitions |
| postDelayed(new Runnable() { |
| @Override |
| public void run() { |
| enableFocusAnimations(); |
| } |
| }, startDelay); |
| } |
| |
| public void fadeInActionButton(int delay, int duration) { |
| // Hide the action button |
| mActionButtonView.setAlpha(0f); |
| |
| // Animate the action button in |
| mActionButtonView.animate().alpha(1f) |
| .setStartDelay(delay) |
| .setDuration(duration) |
| .setInterpolator(PhoneStatusBar.ALPHA_IN) |
| .start(); |
| } |
| |
| /** Animates this task view as it leaves recents by pressing home. */ |
| void startExitToHomeAnimation(ViewAnimation.TaskViewExitContext ctx) { |
| animate() |
| .translationY(ctx.offscreenTranslationY) |
| .setStartDelay(0) |
| .setUpdateListener(null) |
| .setInterpolator(mConfig.fastOutLinearInInterpolator) |
| .setDuration(mConfig.taskViewExitToHomeDuration) |
| .withEndAction(ctx.postAnimationTrigger.decrementAsRunnable()) |
| .start(); |
| ctx.postAnimationTrigger.increment(); |
| } |
| |
| /** Animates this task view away when dismissing all tasks. */ |
| void startDismissAllAnimation() { |
| dismissTask(); |
| } |
| |
| /** Animates this task view as it exits recents */ |
| void startLaunchTaskAnimation(final Runnable postAnimRunnable, boolean isLaunchingTask, |
| boolean occludesLaunchTarget, boolean lockToTask) { |
| if (isLaunchingTask) { |
| // Animate the thumbnail alpha back into full opacity for the window animation out |
| mThumbnailView.startLaunchTaskAnimation(postAnimRunnable); |
| |
| // Animate the dim |
| if (mDimAlpha > 0) { |
| ObjectAnimator anim = ObjectAnimator.ofInt(this, "dim", 0); |
| anim.setDuration(mConfig.taskViewExitToAppDuration); |
| anim.setInterpolator(mConfig.fastOutLinearInInterpolator); |
| anim.start(); |
| } |
| |
| // Animate the action button away |
| if (!lockToTask) { |
| float toScale = 0.9f; |
| mActionButtonView.animate() |
| .scaleX(toScale) |
| .scaleY(toScale); |
| } |
| mActionButtonView.animate() |
| .alpha(0f) |
| .setStartDelay(0) |
| .setDuration(mConfig.taskViewExitToAppDuration) |
| .setInterpolator(mConfig.fastOutLinearInInterpolator) |
| .start(); |
| } else { |
| // If this is another view in the task grouping and is in front of the launch task, |
| // animate it away first |
| if (occludesLaunchTarget) { |
| animate() |
| .alpha(0f) |
| .translationY( |
| getTranslationY() + mConfig.taskViewAffiliateGroupEnterOffsetPx) |
| .setStartDelay(0) |
| .setUpdateListener(null) |
| .setInterpolator(mConfig.fastOutLinearInInterpolator) |
| .setDuration(mConfig.taskViewExitToAppDuration) |
| .start(); |
| } |
| } |
| } |
| |
| /** Animates the deletion of this task view */ |
| void startDeleteTaskAnimation(final Runnable r, int delay) { |
| // Disabling clipping with the stack while the view is animating away |
| setClipViewInStack(false); |
| |
| animate().translationX(mConfig.taskViewRemoveAnimTranslationXPx) |
| .alpha(0f) |
| .setStartDelay(delay) |
| .setUpdateListener(null) |
| .setInterpolator(mConfig.fastOutSlowInInterpolator) |
| .setDuration(mConfig.taskViewRemoveAnimDuration) |
| .withEndAction(new Runnable() { |
| @Override |
| public void run() { |
| if (r != null) { |
| r.run(); |
| } |
| |
| // Re-enable clipping with the stack (we will reuse this view) |
| setClipViewInStack(true); |
| } |
| }) |
| .start(); |
| } |
| |
| /** Enables/disables handling touch on this task view. */ |
| void setTouchEnabled(boolean enabled) { |
| setOnClickListener(enabled ? this : null); |
| } |
| |
| /** Dismisses this task. */ |
| void dismissTask() { |
| // Animate out the view and call the callback |
| final TaskView tv = this; |
| startDeleteTaskAnimation(new Runnable() { |
| @Override |
| public void run() { |
| if (mCb != null) { |
| mCb.onTaskViewDismissed(tv); |
| } |
| } |
| }, 0); |
| } |
| |
| /** |
| * Returns whether this view should be clipped, or any views below should clip against this |
| * view. |
| */ |
| boolean shouldClipViewInStack() { |
| return mClipViewInStack && (getVisibility() == View.VISIBLE); |
| } |
| |
| /** Sets whether this view should be clipped, or clipped against. */ |
| void setClipViewInStack(boolean clip) { |
| if (clip != mClipViewInStack) { |
| mClipViewInStack = clip; |
| if (mCb != null) { |
| mCb.onTaskViewClipStateChanged(this); |
| } |
| } |
| } |
| |
| @Override |
| public void dispatchDraw(Canvas canvas) { |
| if (mFocusAnimatorWasTriggered) { |
| canvas.save(Canvas.CLIP_SAVE_FLAG); |
| mFocusPaint.setAlpha(mFocusBorderAlpha); |
| canvas.clipRect(-mFocusBorderSize, -mFocusBorderSize, |
| getWidth() + mFocusBorderSize, getHeight() + mFocusBorderSize, |
| Region.Op.REPLACE); |
| canvas.drawRoundRect(-mFocusBorderSize, -mFocusBorderSize, |
| getWidth() + mFocusBorderSize, getHeight() + mFocusBorderSize, |
| mConfig.taskViewRoundedCornerRadiusPx, |
| mConfig.taskViewRoundedCornerRadiusPx, mFocusPaint); |
| canvas.restore(); |
| } |
| |
| super.dispatchDraw(canvas); |
| |
| if (mFocusAnimatorWasTriggered) { |
| if (mIsFocused) { |
| final int x = getWidth() / 2; |
| final int y = getHeight() / 2; |
| final float hypot = (float) Math.hypot(x, y); |
| final float radius = hypot * mFocusInCircleRadiusProgress; |
| |
| mFocusPaint.setAlpha(mFocusInFillAlpha); |
| canvas.drawRect(0, 0, getWidth(), getHeight(), mFocusPaint); |
| |
| mFocusPaint.setAlpha(mFocusInCircleAlpha); |
| canvas.drawCircle(x, y, radius, mFocusPaint); |
| } else { |
| mFocusPaint.setAlpha(mFocusOutFillAlpha); |
| canvas.drawRect(0, 0, getWidth(), getHeight(), mFocusPaint); |
| } |
| } |
| } |
| |
| /** Sets the current focus animation progress. Used by the property animator. */ |
| public void setFocusProgress(float progress) { |
| mFocusProgress = progress; |
| |
| if (mIsFocused) { |
| final float interpolatedProgress = sFocusInInterpolator.getInterpolation(progress); |
| |
| mFocusInCircleRadiusProgress = |
| 0.5f + sFocusInRadiusInterpolator.getInterpolation(progress); |
| mFocusInCircleAlpha = |
| (int) (mFocusAlpha * interpolatedProgress); |
| mFocusInFillAlpha = |
| Math.min((mFocusAlpha / 4) + |
| (int) ((mFocusAlpha / 2) * interpolatedProgress), 255); |
| mFocusBorderAlpha = |
| Math.min(mFocusAlpha + (int) (mFocusAlpha * 3f * interpolatedProgress), 255); |
| } else { |
| final float interpolatedProgress = sFocusOutInterpolator.getInterpolation(progress); |
| |
| mFocusOutFillAlpha = |
| (int) (mFocusAlpha * (1 - interpolatedProgress)); |
| mFocusBorderAlpha = |
| Math.min((int) (mFocusAlpha * 4f * (1 - interpolatedProgress)), 255); |
| } |
| |
| invalidate(); |
| } |
| |
| /** Returns the current focus animation progress. */ |
| public float getFocusProgress() { |
| return mFocusProgress; |
| } |
| |
| /** Sets the current task progress. */ |
| public void setTaskProgress(float p) { |
| mTaskProgress = p; |
| mViewBounds.setAlpha(p); |
| updateDimFromTaskProgress(); |
| } |
| |
| /** Returns the current task progress. */ |
| public float getTaskProgress() { |
| return mTaskProgress; |
| } |
| |
| /** Returns the current dim. */ |
| public void setDim(int dim) { |
| mDimAlpha = dim; |
| if (mConfig.useHardwareLayers) { |
| // Defer setting hardware layers if we have not yet measured, or there is no dim to draw |
| if (getMeasuredWidth() > 0 && getMeasuredHeight() > 0) { |
| mDimColorFilter.setColor(Color.argb(mDimAlpha, 0, 0, 0)); |
| mDimLayerPaint.setColorFilter(mDimColorFilter); |
| mContent.setLayerType(LAYER_TYPE_HARDWARE, mDimLayerPaint); |
| } |
| } else { |
| float dimAlpha = mDimAlpha / 255.0f; |
| if (mThumbnailView != null) { |
| mThumbnailView.setDimAlpha(dimAlpha); |
| } |
| if (mHeaderView != null) { |
| mHeaderView.setDimAlpha(dim); |
| } |
| } |
| } |
| |
| /** Returns the current dim. */ |
| public int getDim() { |
| return mDimAlpha; |
| } |
| |
| /** Animates the dim to the task progress. */ |
| void animateDimToProgress(int delay, int duration, Animator.AnimatorListener postAnimRunnable) { |
| // Animate the dim into view as well |
| int toDim = getDimFromTaskProgress(); |
| if (toDim != getDim()) { |
| ObjectAnimator anim = ObjectAnimator.ofInt(TaskView.this, "dim", toDim); |
| anim.setStartDelay(delay); |
| anim.setDuration(duration); |
| if (postAnimRunnable != null) { |
| anim.addListener(postAnimRunnable); |
| } |
| anim.start(); |
| } |
| } |
| |
| /** Compute the dim as a function of the scale of this view. */ |
| int getDimFromTaskProgress() { |
| float dim = mMaxDimScale * mDimInterpolator.getInterpolation(1f - mTaskProgress); |
| return (int) (dim * 255); |
| } |
| |
| /** Update the dim as a function of the scale of this view. */ |
| void updateDimFromTaskProgress() { |
| setDim(getDimFromTaskProgress()); |
| } |
| |
| /**** View focus state ****/ |
| |
| /** |
| * Sets the focused task explicitly. We need a separate flag because requestFocus() won't happen |
| * if the view is not currently visible, or we are in touch state (where we still want to keep |
| * track of focus). |
| */ |
| public void setFocusedTask() { |
| if (mIsFocused) { |
| return; |
| } |
| mIsFocused = true; |
| |
| performFocusAnimation(); |
| // Update the thumbnail alpha with the focus |
| mThumbnailView.onFocusChanged(true); |
| // Call the callback |
| if (mCb != null) { |
| mCb.onTaskViewFocusChanged(this, true); |
| } |
| // Workaround, we don't always want it focusable in touch mode, but we want the first task |
| // to be focused after the enter-recents animation, which can be triggered from either touch |
| // or keyboard |
| setFocusableInTouchMode(true); |
| requestFocus(); |
| setFocusableInTouchMode(false); |
| invalidate(); |
| } |
| |
| /** Performs the focus animation for alt-tab traversal. */ |
| private void performFocusAnimation() { |
| if (mFocusAnimationsEnabled) { |
| // Focus the header bar |
| mHeaderView.onTaskViewFocusChanged(true); |
| |
| mFocusAnimator.setDuration(sFocusInDurationMs); |
| mFocusAnimator.start(); |
| } |
| } |
| |
| /** |
| * Unsets the focused task explicitly. |
| */ |
| void unsetFocusedTask() { |
| if (!mIsFocused) { |
| return; |
| } |
| |
| mIsFocused = false; |
| if (mFocusAnimationsEnabled) { |
| // Un-focus the header bar |
| mHeaderView.onTaskViewFocusChanged(false); |
| |
| mFocusAnimator.setDuration(sFocusOutDurationMs); |
| mFocusAnimator.start(); |
| } |
| |
| // Update the thumbnail alpha with the focus |
| mThumbnailView.onFocusChanged(false); |
| // Call the callback |
| if (mCb != null) { |
| mCb.onTaskViewFocusChanged(this, false); |
| } |
| invalidate(); |
| } |
| |
| /** |
| * Updates the explicitly focused state when the view focus changes. |
| */ |
| @Override |
| protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { |
| super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); |
| if (!gainFocus) { |
| unsetFocusedTask(); |
| } |
| } |
| |
| /** |
| * Returns whether we have explicitly been focused. |
| * This only determines whether or not focus is being drawn. |
| */ |
| public boolean isFocusedTask() { |
| return mIsFocused; |
| } |
| |
| /** Enables all focus animations. */ |
| void enableFocusAnimations() { |
| boolean wasFocusAnimationsEnabled = mFocusAnimationsEnabled; |
| mFocusAnimationsEnabled = true; |
| if (mIsFocused && !wasFocusAnimationsEnabled) { |
| // Re-notify the header if we were focused and animations were not previously enabled |
| performFocusAnimation(); |
| } |
| } |
| |
| /** Disables focus animations and resets focus state. */ |
| void disableFocusAnimations() { |
| mFocusAnimationsEnabled = false; |
| mIsFocused = false; |
| mFocusAnimatorWasTriggered = false; |
| } |
| |
| public void disableLayersForOneFrame() { |
| mHeaderView.disableLayersForOneFrame(); |
| } |
| |
| /**** TaskCallbacks Implementation ****/ |
| |
| /** Binds this task view to the task */ |
| public void onTaskBound(Task t) { |
| mTask = t; |
| mTask.setCallbacks(this); |
| |
| // Hide the action button if lock to app is disabled for this view |
| int lockButtonVisibility = (!t.lockToTaskEnabled || !t.lockToThisTask) ? GONE : VISIBLE; |
| if (mActionButtonView.getVisibility() != lockButtonVisibility) { |
| mActionButtonView.setVisibility(lockButtonVisibility); |
| requestLayout(); |
| } |
| } |
| |
| @Override |
| public void onTaskDataLoaded() { |
| if (mThumbnailView != null && mHeaderView != null) { |
| // Bind each of the views to the new task data |
| mThumbnailView.rebindToTask(mTask); |
| mHeaderView.rebindToTask(mTask); |
| // Rebind any listeners |
| AccessibilityManager am = (AccessibilityManager) getContext(). |
| getSystemService(Context.ACCESSIBILITY_SERVICE); |
| if (Constants.DebugFlags.App.EnableTaskFiltering || (am != null && am.isEnabled())) { |
| mHeaderView.mApplicationIcon.setOnClickListener(this); |
| } |
| mHeaderView.mDismissButton.setOnClickListener(this); |
| if (mConfig.multiStackEnabled) { |
| mHeaderView.mMoveTaskButton.setOnClickListener(this); |
| } |
| mActionButtonView.setOnClickListener(this); |
| mHeaderView.mApplicationIcon.setOnLongClickListener(this); |
| } |
| mTaskDataLoaded = true; |
| } |
| |
| @Override |
| public void onTaskDataUnloaded() { |
| if (mThumbnailView != null && mHeaderView != null) { |
| // Unbind each of the views from the task data and remove the task callback |
| mTask.setCallbacks(null); |
| mThumbnailView.unbindFromTask(); |
| mHeaderView.unbindFromTask(); |
| // Unbind any listeners |
| mHeaderView.mApplicationIcon.setOnClickListener(null); |
| mHeaderView.mDismissButton.setOnClickListener(null); |
| if (mConfig.multiStackEnabled) { |
| mHeaderView.mMoveTaskButton.setOnClickListener(null); |
| } |
| mActionButtonView.setOnClickListener(null); |
| mHeaderView.mApplicationIcon.setOnLongClickListener(null); |
| } |
| mTaskDataLoaded = false; |
| } |
| |
| @Override |
| public void onMultiStackDebugTaskStackIdChanged() { |
| mHeaderView.rebindToTask(mTask); |
| } |
| |
| /**** View.OnClickListener Implementation ****/ |
| |
| @Override |
| public void onClick(final View v) { |
| final TaskView tv = this; |
| final boolean delayViewClick = (v != this) && (v != mActionButtonView); |
| if (delayViewClick) { |
| // We purposely post the handler delayed to allow for the touch feedback to draw |
| postDelayed(new Runnable() { |
| @Override |
| public void run() { |
| if (v == mHeaderView.mApplicationIcon) { |
| if (Constants.DebugFlags.App.EnableTaskFiltering) { |
| if (mCb != null) { |
| mCb.onTaskViewAppIconClicked(tv); |
| } |
| } else { |
| AccessibilityManager am = (AccessibilityManager) getContext(). |
| getSystemService(Context.ACCESSIBILITY_SERVICE); |
| if (am != null && am.isEnabled()) { |
| if (mCb != null) { |
| mCb.onTaskViewAppInfoClicked(tv); |
| } |
| } |
| } |
| } else if (v == mHeaderView.mDismissButton) { |
| dismissTask(); |
| // Keep track of deletions by the dismiss button |
| MetricsLogger.histogram(getContext(), "overview_task_dismissed_source", |
| Constants.Metrics.DismissSourceHeaderButton); |
| } else if (v == mHeaderView.mMoveTaskButton) { |
| if (mCb != null) { |
| mCb.onTaskResize(tv); |
| } |
| } |
| } |
| }, 125); |
| } else { |
| if (v == mActionButtonView) { |
| // Reset the translation of the action button before we animate it out |
| mActionButtonView.setTranslationZ(0f); |
| } |
| if (mCb != null) { |
| mCb.onTaskViewClicked(tv, tv.getTask(), (v == mActionButtonView)); |
| } |
| } |
| } |
| |
| /**** View.OnLongClickListener Implementation ****/ |
| |
| @Override |
| public boolean onLongClick(View v) { |
| if (v == mHeaderView.mApplicationIcon) { |
| if (mCb != null) { |
| mCb.onTaskViewAppInfoClicked(this); |
| return true; |
| } |
| } |
| return false; |
| } |
| } |