TaskViewHeader.java revision 2536c7ed446203ea12b38cf05a88e603f8d1b768
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.systemui.recents.views; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.AnimatorSet; 22import android.animation.ArgbEvaluator; 23import android.animation.ObjectAnimator; 24import android.animation.ValueAnimator; 25import android.content.Context; 26import android.content.res.ColorStateList; 27import android.graphics.Canvas; 28import android.graphics.Color; 29import android.graphics.Outline; 30import android.graphics.Paint; 31import android.graphics.PorterDuff; 32import android.graphics.PorterDuffColorFilter; 33import android.graphics.PorterDuffXfermode; 34import android.graphics.Rect; 35import android.graphics.drawable.ColorDrawable; 36import android.graphics.drawable.Drawable; 37import android.graphics.drawable.GradientDrawable; 38import android.graphics.drawable.RippleDrawable; 39import android.util.AttributeSet; 40import android.view.View; 41import android.view.ViewOutlineProvider; 42import android.view.accessibility.AccessibilityManager; 43import android.view.animation.AnimationUtils; 44import android.view.animation.Interpolator; 45import android.widget.FrameLayout; 46import android.widget.ImageView; 47import android.widget.TextView; 48import com.android.internal.logging.MetricsLogger; 49import com.android.systemui.R; 50import com.android.systemui.recents.Constants; 51import com.android.systemui.recents.RecentsConfiguration; 52import com.android.systemui.recents.events.EventBus; 53import com.android.systemui.recents.events.ui.ResizeTaskEvent; 54import com.android.systemui.recents.events.ui.ShowApplicationInfoEvent; 55import com.android.systemui.recents.misc.SystemServicesProxy; 56import com.android.systemui.recents.misc.Utilities; 57import com.android.systemui.recents.model.RecentsTaskLoader; 58import com.android.systemui.recents.model.Task; 59 60 61/* The task bar view */ 62public class TaskViewHeader extends FrameLayout 63 implements View.OnClickListener, View.OnLongClickListener { 64 65 RecentsConfiguration mConfig; 66 private SystemServicesProxy mSsp; 67 Task mTask; 68 69 // Header views 70 ImageView mMoveTaskButton; 71 ImageView mDismissButton; 72 ImageView mApplicationIcon; 73 TextView mActivityDescription; 74 75 // Header drawables 76 boolean mCurrentPrimaryColorIsDark; 77 int mCurrentPrimaryColor; 78 int mBackgroundColor; 79 int mCornerRadius; 80 int mHighlightHeight; 81 Drawable mLightDismissDrawable; 82 Drawable mDarkDismissDrawable; 83 RippleDrawable mBackground; 84 GradientDrawable mBackgroundColorDrawable; 85 AnimatorSet mFocusAnimator; 86 String mDismissContentDescription; 87 88 // Static highlight that we draw at the top of each view 89 static Paint sHighlightPaint; 90 91 // Header dim, which is only used when task view hardware layers are not used 92 Paint mDimLayerPaint = new Paint(); 93 PorterDuffColorFilter mDimColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_ATOP); 94 95 Interpolator mFastOutSlowInInterpolator; 96 Interpolator mFastOutLinearInInterpolator; 97 98 boolean mLayersDisabled; 99 100 public TaskViewHeader(Context context) { 101 this(context, null); 102 } 103 104 public TaskViewHeader(Context context, AttributeSet attrs) { 105 this(context, attrs, 0); 106 } 107 108 public TaskViewHeader(Context context, AttributeSet attrs, int defStyleAttr) { 109 this(context, attrs, defStyleAttr, 0); 110 } 111 112 public TaskViewHeader(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 113 super(context, attrs, defStyleAttr, defStyleRes); 114 mConfig = RecentsConfiguration.getInstance(); 115 mSsp = RecentsTaskLoader.getInstance().getSystemServicesProxy(); 116 setWillNotDraw(false); 117 setClipToOutline(true); 118 setOutlineProvider(new ViewOutlineProvider() { 119 @Override 120 public void getOutline(View view, Outline outline) { 121 outline.setRect(0, 0, getMeasuredWidth(), getMeasuredHeight()); 122 } 123 }); 124 125 // Load the dismiss resources 126 mLightDismissDrawable = context.getDrawable(R.drawable.recents_dismiss_light); 127 mDarkDismissDrawable = context.getDrawable(R.drawable.recents_dismiss_dark); 128 mDismissContentDescription = 129 context.getString(R.string.accessibility_recents_item_will_be_dismissed); 130 mCornerRadius = getResources().getDimensionPixelSize( 131 R.dimen.recents_task_view_rounded_corners_radius); 132 mHighlightHeight = getResources().getDimensionPixelSize( 133 R.dimen.recents_task_view_highlight); 134 mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context, 135 com.android.internal.R.interpolator.fast_out_slow_in); 136 mFastOutLinearInInterpolator = AnimationUtils.loadInterpolator(context, 137 com.android.internal.R.interpolator.fast_out_linear_in); 138 139 // Configure the highlight paint 140 if (sHighlightPaint == null) { 141 sHighlightPaint = new Paint(); 142 sHighlightPaint.setStyle(Paint.Style.STROKE); 143 sHighlightPaint.setStrokeWidth(mHighlightHeight); 144 sHighlightPaint.setColor(context.getColor(R.color.recents_task_bar_highlight_color)); 145 sHighlightPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.ADD)); 146 sHighlightPaint.setAntiAlias(true); 147 } 148 } 149 150 @Override 151 protected void onFinishInflate() { 152 // Initialize the icon and description views 153 mApplicationIcon = (ImageView) findViewById(R.id.application_icon); 154 mApplicationIcon.setOnLongClickListener(this); 155 mActivityDescription = (TextView) findViewById(R.id.activity_description); 156 mDismissButton = (ImageView) findViewById(R.id.dismiss_task); 157 mDismissButton.setOnClickListener(this); 158 mMoveTaskButton = (ImageView) findViewById(R.id.move_task); 159 160 // Hide the backgrounds if they are ripple drawables 161 if (mApplicationIcon.getBackground() instanceof RippleDrawable) { 162 mApplicationIcon.setBackground(null); 163 } 164 165 mBackgroundColorDrawable = (GradientDrawable) getContext().getDrawable(R.drawable 166 .recents_task_view_header_bg_color); 167 // Copy the ripple drawable since we are going to be manipulating it 168 mBackground = (RippleDrawable) 169 getContext().getDrawable(R.drawable.recents_task_view_header_bg); 170 mBackground = (RippleDrawable) mBackground.mutate().getConstantState().newDrawable(); 171 mBackground.setColor(ColorStateList.valueOf(0)); 172 mBackground.setDrawableByLayerId(mBackground.getId(0), mBackgroundColorDrawable); 173 setBackground(mBackground); 174 } 175 176 @Override 177 protected void onDraw(Canvas canvas) { 178 // Draw the highlight at the top edge (but put the bottom edge just out of view) 179 float offset = (float) Math.ceil(mHighlightHeight / 2f); 180 float radius = mCornerRadius; 181 int count = canvas.save(Canvas.CLIP_SAVE_FLAG); 182 canvas.clipRect(0, 0, getMeasuredWidth(), getMeasuredHeight()); 183 canvas.drawRoundRect(-offset, 0f, (float) getMeasuredWidth() + offset, 184 getMeasuredHeight() + radius, radius, radius, sHighlightPaint); 185 canvas.restoreToCount(count); 186 } 187 188 @Override 189 public boolean hasOverlappingRendering() { 190 return false; 191 } 192 193 /** 194 * Sets the dim alpha, only used when we are not using hardware layers. 195 * (see RecentsConfiguration.useHardwareLayers) 196 */ 197 void setDimAlpha(int alpha) { 198 mDimColorFilter.setColor(Color.argb(alpha, 0, 0, 0)); 199 mDimLayerPaint.setColorFilter(mDimColorFilter); 200 if (!mLayersDisabled) { 201 setLayerType(LAYER_TYPE_HARDWARE, mDimLayerPaint); 202 } 203 } 204 205 /** Returns the secondary color for a primary color. */ 206 int getSecondaryColor(int primaryColor, boolean useLightOverlayColor) { 207 int overlayColor = useLightOverlayColor ? Color.WHITE : Color.BLACK; 208 return Utilities.getColorWithOverlay(primaryColor, overlayColor, 0.8f); 209 } 210 211 /** Binds the bar view to the task */ 212 public void rebindToTask(Task t) { 213 mTask = t; 214 215 // If an activity icon is defined, then we use that as the primary icon to show in the bar, 216 // otherwise, we fall back to the application icon 217 if (t.activityIcon != null) { 218 mApplicationIcon.setImageDrawable(t.activityIcon); 219 } else if (t.applicationIcon != null) { 220 mApplicationIcon.setImageDrawable(t.applicationIcon); 221 } 222 if (!mActivityDescription.getText().toString().equals(t.activityLabel)) { 223 mActivityDescription.setText(t.activityLabel); 224 } 225 mActivityDescription.setContentDescription(t.contentDescription); 226 227 // Try and apply the system ui tint 228 int existingBgColor = (getBackground() instanceof ColorDrawable) ? 229 ((ColorDrawable) getBackground()).getColor() : 0; 230 if (existingBgColor != t.colorPrimary) { 231 mBackgroundColorDrawable.setColor(t.colorPrimary); 232 mBackgroundColor = t.colorPrimary; 233 } 234 235 int taskBarViewLightTextColor = getResources().getColor( 236 R.color.recents_task_bar_light_text_color); 237 int taskBarViewDarkTextColor = getResources().getColor( 238 R.color.recents_task_bar_dark_text_color); 239 mCurrentPrimaryColor = t.colorPrimary; 240 mCurrentPrimaryColorIsDark = t.useLightOnPrimaryColor; 241 mActivityDescription.setTextColor(t.useLightOnPrimaryColor ? 242 taskBarViewLightTextColor : taskBarViewDarkTextColor); 243 mDismissButton.setImageDrawable(t.useLightOnPrimaryColor ? 244 mLightDismissDrawable : mDarkDismissDrawable); 245 mDismissButton.setContentDescription(String.format(mDismissContentDescription, 246 t.contentDescription)); 247 mMoveTaskButton.setVisibility((mConfig.multiWindowEnabled) ? View.VISIBLE : View.INVISIBLE); 248 if (mConfig.multiWindowEnabled) { 249 updateResizeTaskBarIcon(t); 250 mMoveTaskButton.setOnClickListener(this); 251 } 252 253 // In accessibility, a single click on the focused app info button will show it 254 AccessibilityManager am = (AccessibilityManager) getContext(). 255 getSystemService(Context.ACCESSIBILITY_SERVICE); 256 if (am != null && am.isEnabled()) { 257 mApplicationIcon.setOnClickListener(this); 258 } 259 } 260 261 /** Unbinds the bar view from the task */ 262 void unbindFromTask() { 263 mTask = null; 264 mApplicationIcon.setImageDrawable(null); 265 mApplicationIcon.setOnClickListener(null); 266 267 if (mConfig.multiWindowEnabled) { 268 mMoveTaskButton.setOnClickListener(null); 269 } 270 } 271 272 /** Updates the resize task bar button. */ 273 void updateResizeTaskBarIcon(Task t) { 274 Rect display = mSsp.getWindowRect(); 275 Rect taskRect = mSsp.getTaskBounds(t.key.id); 276 int resId = R.drawable.star; 277 if (display.equals(taskRect) || taskRect.isEmpty()) { 278 resId = R.drawable.vector_drawable_place_fullscreen; 279 } else { 280 boolean top = display.top == taskRect.top; 281 boolean bottom = display.bottom == taskRect.bottom; 282 boolean left = display.left == taskRect.left; 283 boolean right = display.right == taskRect.right; 284 if (top && bottom && left) { 285 resId = R.drawable.vector_drawable_place_left; 286 } else if (top && bottom && right) { 287 resId = R.drawable.vector_drawable_place_right; 288 } else if (top && left && right) { 289 resId = R.drawable.vector_drawable_place_top; 290 } else if (bottom && left && right) { 291 resId = R.drawable.vector_drawable_place_bottom; 292 } else if (top && right) { 293 resId = R.drawable.vector_drawable_place_top_right; 294 } else if (top && left) { 295 resId = R.drawable.vector_drawable_place_top_left; 296 } else if (bottom && right) { 297 resId = R.drawable.vector_drawable_place_bottom_right; 298 } else if (bottom && left) { 299 resId = R.drawable.vector_drawable_place_bottom_left; 300 } 301 } 302 mMoveTaskButton.setImageResource(resId); 303 } 304 305 /** Animates this task bar dismiss button when launching a task. */ 306 void startLaunchTaskDismissAnimation() { 307 if (mDismissButton.getVisibility() == View.VISIBLE) { 308 int taskViewExitToAppDuration = mContext.getResources().getInteger( 309 R.integer.recents_task_exit_to_app_duration); 310 mDismissButton.animate().cancel(); 311 mDismissButton.animate() 312 .alpha(0f) 313 .setStartDelay(0) 314 .setInterpolator(mFastOutSlowInInterpolator) 315 .setDuration(taskViewExitToAppDuration) 316 .start(); 317 } 318 } 319 320 /** Animates this task bar if the user does not interact with the stack after a certain time. */ 321 void startNoUserInteractionAnimation() { 322 if (mDismissButton.getVisibility() != View.VISIBLE) { 323 mDismissButton.setVisibility(View.VISIBLE); 324 mDismissButton.setAlpha(0f); 325 mDismissButton.animate() 326 .alpha(1f) 327 .setStartDelay(0) 328 .setInterpolator(mFastOutLinearInInterpolator) 329 .setDuration(getResources().getInteger( 330 R.integer.recents_task_enter_from_app_duration)) 331 .start(); 332 } 333 } 334 335 /** Mark this task view that the user does has not interacted with the stack after a certain time. */ 336 void setNoUserInteractionState() { 337 if (mDismissButton.getVisibility() != View.VISIBLE) { 338 mDismissButton.animate().cancel(); 339 mDismissButton.setVisibility(View.VISIBLE); 340 mDismissButton.setAlpha(1f); 341 } 342 } 343 344 /** Resets the state tracking that the user has not interacted with the stack after a certain time. */ 345 void resetNoUserInteractionState() { 346 mDismissButton.setVisibility(View.INVISIBLE); 347 } 348 349 @Override 350 protected int[] onCreateDrawableState(int extraSpace) { 351 352 // Don't forward our state to the drawable - we do it manually in onTaskViewFocusChanged. 353 // This is to prevent layer trashing when the view is pressed. 354 return new int[] {}; 355 } 356 357 @Override 358 protected void dispatchDraw(Canvas canvas) { 359 super.dispatchDraw(canvas); 360 if (mLayersDisabled) { 361 mLayersDisabled = false; 362 postOnAnimation(new Runnable() { 363 @Override 364 public void run() { 365 mLayersDisabled = false; 366 setLayerType(LAYER_TYPE_HARDWARE, mDimLayerPaint); 367 } 368 }); 369 } 370 } 371 372 public void disableLayersForOneFrame() { 373 mLayersDisabled = true; 374 375 // Disable layer for a frame so we can draw our first frame faster. 376 setLayerType(LAYER_TYPE_NONE, null); 377 } 378 379 /** Notifies the associated TaskView has been focused. */ 380 void onTaskViewFocusChanged(boolean focused, boolean animateFocusedState) { 381 // If we are not animating the visible state, just return 382 if (!animateFocusedState) return; 383 384 boolean isRunning = false; 385 if (mFocusAnimator != null) { 386 isRunning = mFocusAnimator.isRunning(); 387 Utilities.cancelAnimationWithoutCallbacks(mFocusAnimator); 388 } 389 390 if (focused) { 391 int currentColor = mBackgroundColor; 392 int secondaryColor = getSecondaryColor(mCurrentPrimaryColor, mCurrentPrimaryColorIsDark); 393 int[][] states = new int[][] { 394 new int[] {}, 395 new int[] { android.R.attr.state_enabled }, 396 new int[] { android.R.attr.state_pressed } 397 }; 398 int[] newStates = new int[]{ 399 0, 400 android.R.attr.state_enabled, 401 android.R.attr.state_pressed 402 }; 403 int[] colors = new int[] { 404 currentColor, 405 secondaryColor, 406 secondaryColor 407 }; 408 mBackground.setColor(new ColorStateList(states, colors)); 409 mBackground.setState(newStates); 410 // Pulse the background color 411 int lightPrimaryColor = getSecondaryColor(mCurrentPrimaryColor, mCurrentPrimaryColorIsDark); 412 ValueAnimator backgroundColor = ValueAnimator.ofObject(new ArgbEvaluator(), 413 currentColor, lightPrimaryColor); 414 backgroundColor.addListener(new AnimatorListenerAdapter() { 415 @Override 416 public void onAnimationStart(Animator animation) { 417 mBackground.setState(new int[]{}); 418 } 419 }); 420 backgroundColor.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 421 @Override 422 public void onAnimationUpdate(ValueAnimator animation) { 423 int color = (int) animation.getAnimatedValue(); 424 mBackgroundColorDrawable.setColor(color); 425 mBackgroundColor = color; 426 } 427 }); 428 backgroundColor.setRepeatCount(ValueAnimator.INFINITE); 429 backgroundColor.setRepeatMode(ValueAnimator.REVERSE); 430 // Pulse the translation 431 ObjectAnimator translation = ObjectAnimator.ofFloat(this, "translationZ", 15f); 432 translation.setRepeatCount(ValueAnimator.INFINITE); 433 translation.setRepeatMode(ValueAnimator.REVERSE); 434 435 mFocusAnimator = new AnimatorSet(); 436 mFocusAnimator.playTogether(backgroundColor, translation); 437 mFocusAnimator.setStartDelay(150); 438 mFocusAnimator.setDuration(750); 439 mFocusAnimator.start(); 440 } else { 441 if (isRunning) { 442 // Restore the background color 443 int currentColor = mBackgroundColor; 444 ValueAnimator backgroundColor = ValueAnimator.ofObject(new ArgbEvaluator(), 445 currentColor, mCurrentPrimaryColor); 446 backgroundColor.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 447 @Override 448 public void onAnimationUpdate(ValueAnimator animation) { 449 int color = (int) animation.getAnimatedValue(); 450 mBackgroundColorDrawable.setColor(color); 451 mBackgroundColor = color; 452 } 453 }); 454 // Restore the translation 455 ObjectAnimator translation = ObjectAnimator.ofFloat(this, "translationZ", 0f); 456 457 mFocusAnimator = new AnimatorSet(); 458 mFocusAnimator.playTogether(backgroundColor, translation); 459 mFocusAnimator.setDuration(150); 460 mFocusAnimator.start(); 461 } else { 462 mBackground.setState(new int[] {}); 463 setTranslationZ(0f); 464 } 465 } 466 } 467 468 @Override 469 public void onClick(View v) { 470 if (v == mApplicationIcon) { 471 // In accessibility, a single click on the focused app info button will show it 472 EventBus.getDefault().send(new ShowApplicationInfoEvent(mTask)); 473 } else if (v == mDismissButton) { 474 TaskView tv = (TaskView) getParent().getParent(); 475 tv.dismissTask(); 476 477 // Keep track of deletions by the dismiss button 478 MetricsLogger.histogram(getContext(), "overview_task_dismissed_source", 479 Constants.Metrics.DismissSourceHeaderButton); 480 } else if (v == mMoveTaskButton) { 481 EventBus.getDefault().send(new ResizeTaskEvent(mTask)); 482 } 483 } 484 485 @Override 486 public boolean onLongClick(View v) { 487 if (v == mApplicationIcon) { 488 EventBus.getDefault().send(new ShowApplicationInfoEvent(mTask)); 489 return true; 490 } 491 return false; 492 } 493} 494