1 /* 2 * Copyright (C) 2011 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.launcher3; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.app.ActivityOptions; 22 import android.content.Context; 23 import android.content.ComponentName; 24 import android.content.Intent; 25 import android.content.res.Resources; 26 import android.content.res.TypedArray; 27 import android.graphics.*; 28 import android.graphics.drawable.Drawable; 29 import android.util.AttributeSet; 30 import android.util.DisplayMetrics; 31 import android.view.FocusFinder; 32 import android.view.MotionEvent; 33 import android.view.View; 34 import android.view.animation.AccelerateInterpolator; 35 import android.widget.FrameLayout; 36 import android.widget.TextView; 37 38 public class Cling extends FrameLayout implements Insettable, View.OnClickListener, 39 View.OnLongClickListener, View.OnTouchListener { 40 41 static final String FIRST_RUN_CLING_DISMISSED_KEY = "cling_gel.first_run.dismissed"; 42 static final String WORKSPACE_CLING_DISMISSED_KEY = "cling_gel.workspace.dismissed"; 43 static final String FOLDER_CLING_DISMISSED_KEY = "cling_gel.folder.dismissed"; 44 45 private static String FIRST_RUN_PORTRAIT = "first_run_portrait"; 46 private static String FIRST_RUN_LANDSCAPE = "first_run_landscape"; 47 48 private static String WORKSPACE_PORTRAIT = "workspace_portrait"; 49 private static String WORKSPACE_LANDSCAPE = "workspace_landscape"; 50 private static String WORKSPACE_LARGE = "workspace_large"; 51 private static String WORKSPACE_CUSTOM = "workspace_custom"; 52 53 private static String FOLDER_PORTRAIT = "folder_portrait"; 54 private static String FOLDER_LANDSCAPE = "folder_landscape"; 55 private static String FOLDER_LARGE = "folder_large"; 56 57 private static float FIRST_RUN_CIRCLE_BUFFER_DPS = 60; 58 private static float WORKSPACE_INNER_CIRCLE_RADIUS_DPS = 50; 59 private static float WORKSPACE_OUTER_CIRCLE_RADIUS_DPS = 60; 60 private static float WORKSPACE_CIRCLE_Y_OFFSET_DPS = 30; 61 62 private Launcher mLauncher; 63 private boolean mIsInitialized; 64 private String mDrawIdentifier; 65 private Drawable mBackground; 66 67 private int[] mTouchDownPt = new int[2]; 68 69 private Drawable mFocusedHotseatApp; 70 private ComponentName mFocusedHotseatAppComponent; 71 private Rect mFocusedHotseatAppBounds; 72 73 private Paint mErasePaint; 74 private Paint mBubblePaint; 75 private Paint mDotPaint; 76 77 private View mScrimView; 78 private int mBackgroundColor; 79 80 private final Rect mInsets = new Rect(); 81 82 public Cling(Context context) { 83 this(context, null, 0); 84 } 85 86 public Cling(Context context, AttributeSet attrs) { 87 this(context, attrs, 0); 88 } 89 90 public Cling(Context context, AttributeSet attrs, int defStyle) { 91 super(context, attrs, defStyle); 92 93 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Cling, defStyle, 0); 94 mDrawIdentifier = a.getString(R.styleable.Cling_drawIdentifier); 95 a.recycle(); 96 97 setClickable(true); 98 99 } 100 101 void init(Launcher l, View scrim) { 102 if (!mIsInitialized) { 103 mLauncher = l; 104 mScrimView = scrim; 105 mBackgroundColor = 0xdd000000; 106 setOnLongClickListener(this); 107 setOnClickListener(this); 108 setOnTouchListener(this); 109 110 mErasePaint = new Paint(); 111 mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.MULTIPLY)); 112 mErasePaint.setColor(0xFFFFFF); 113 mErasePaint.setAlpha(0); 114 mErasePaint.setAntiAlias(true); 115 116 int circleColor = getResources().getColor( 117 R.color.first_run_cling_circle_background_color); 118 mBubblePaint = new Paint(); 119 mBubblePaint.setColor(circleColor); 120 mBubblePaint.setAntiAlias(true); 121 122 mDotPaint = new Paint(); 123 mDotPaint.setColor(0x72BBED); 124 mDotPaint.setAntiAlias(true); 125 126 mIsInitialized = true; 127 } 128 } 129 130 void setFocusedHotseatApp(int drawableId, int appRank, ComponentName cn, String title, 131 String description) { 132 // Get the app to draw 133 Resources r = getResources(); 134 int appIconId = drawableId; 135 Hotseat hotseat = mLauncher.getHotseat(); 136 if (hotseat != null && appIconId > -1 && appRank > -1 && !title.isEmpty() && 137 !description.isEmpty()) { 138 // Set the app bounds 139 int x = hotseat.getCellXFromOrder(appRank); 140 int y = hotseat.getCellYFromOrder(appRank); 141 Rect pos = hotseat.getCellCoordinates(x, y); 142 LauncherAppState app = LauncherAppState.getInstance(); 143 DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); 144 mFocusedHotseatApp = getResources().getDrawable(appIconId); 145 mFocusedHotseatAppComponent = cn; 146 mFocusedHotseatAppBounds = new Rect(pos.left, pos.top, 147 pos.left + Utilities.sIconTextureWidth, 148 pos.top + Utilities.sIconTextureHeight); 149 Utilities.scaleRectAboutCenter(mFocusedHotseatAppBounds, 150 (grid.hotseatIconSize / grid.iconSize)); 151 152 // Set the title 153 TextView v = (TextView) findViewById(R.id.focused_hotseat_app_title); 154 if (v != null) { 155 v.setText(title); 156 } 157 158 // Set the description 159 v = (TextView) findViewById(R.id.focused_hotseat_app_description); 160 if (v != null) { 161 v.setText(description); 162 } 163 164 // Show the bubble 165 View bubble = findViewById(R.id.focused_hotseat_app_bubble); 166 bubble.setVisibility(View.VISIBLE); 167 } 168 } 169 170 void show(boolean animate, int duration) { 171 setVisibility(View.VISIBLE); 172 setLayerType(View.LAYER_TYPE_HARDWARE, null); 173 if (mDrawIdentifier.equals(WORKSPACE_PORTRAIT) || 174 mDrawIdentifier.equals(WORKSPACE_LANDSCAPE) || 175 mDrawIdentifier.equals(WORKSPACE_LARGE) || 176 mDrawIdentifier.equals(WORKSPACE_CUSTOM)) { 177 View content = getContent(); 178 content.setAlpha(0f); 179 content.animate() 180 .alpha(1f) 181 .setDuration(duration) 182 .setListener(null) 183 .start(); 184 setAlpha(1f); 185 } else { 186 if (animate) { 187 buildLayer(); 188 setAlpha(0f); 189 animate() 190 .alpha(1f) 191 .setInterpolator(new AccelerateInterpolator()) 192 .setDuration(duration) 193 .setListener(null) 194 .start(); 195 } else { 196 setAlpha(1f); 197 } 198 } 199 200 // Show the scrim if necessary 201 if (mScrimView != null) { 202 mScrimView.setVisibility(View.VISIBLE); 203 mScrimView.setAlpha(0f); 204 mScrimView.animate() 205 .alpha(1f) 206 .setDuration(duration) 207 .setListener(null) 208 .start(); 209 } 210 211 setFocusableInTouchMode(true); 212 post(new Runnable() { 213 public void run() { 214 setFocusable(true); 215 requestFocus(); 216 } 217 }); 218 } 219 220 void hide(final int duration, final Runnable postCb) { 221 if (mDrawIdentifier.equals(FIRST_RUN_PORTRAIT) || 222 mDrawIdentifier.equals(FIRST_RUN_LANDSCAPE)) { 223 View content = getContent(); 224 content.animate() 225 .alpha(0f) 226 .setDuration(duration) 227 .setListener(new AnimatorListenerAdapter() { 228 public void onAnimationEnd(Animator animation) { 229 // We are about to trigger the workspace cling, so don't do anything else 230 setVisibility(View.GONE); 231 postCb.run(); 232 }; 233 }) 234 .start(); 235 } else { 236 animate() 237 .alpha(0f) 238 .setDuration(duration) 239 .setListener(new AnimatorListenerAdapter() { 240 public void onAnimationEnd(Animator animation) { 241 // We are about to trigger the workspace cling, so don't do anything else 242 setVisibility(View.GONE); 243 postCb.run(); 244 }; 245 }) 246 .start(); 247 } 248 249 // Show the scrim if necessary 250 if (mScrimView != null) { 251 mScrimView.animate() 252 .alpha(0f) 253 .setDuration(duration) 254 .setListener(new AnimatorListenerAdapter() { 255 public void onAnimationEnd(Animator animation) { 256 mScrimView.setVisibility(View.GONE); 257 }; 258 }) 259 .start(); 260 } 261 } 262 263 void cleanup() { 264 mBackground = null; 265 mIsInitialized = false; 266 } 267 268 void bringScrimToFront() { 269 if (mScrimView != null) { 270 mScrimView.bringToFront(); 271 } 272 } 273 274 @Override 275 public void setInsets(Rect insets) { 276 mInsets.set(insets); 277 setPadding(insets.left, insets.top, insets.right, insets.bottom); 278 } 279 280 View getContent() { 281 return findViewById(R.id.content); 282 } 283 284 String getDrawIdentifier() { 285 return mDrawIdentifier; 286 } 287 288 @Override 289 public View focusSearch(int direction) { 290 return this.focusSearch(this, direction); 291 } 292 293 @Override 294 public View focusSearch(View focused, int direction) { 295 return FocusFinder.getInstance().findNextFocus(this, focused, direction); 296 } 297 298 @Override 299 public boolean onHoverEvent(MotionEvent event) { 300 return (mDrawIdentifier.equals(WORKSPACE_PORTRAIT) 301 || mDrawIdentifier.equals(WORKSPACE_LANDSCAPE) 302 || mDrawIdentifier.equals(WORKSPACE_LARGE) 303 || mDrawIdentifier.equals(WORKSPACE_CUSTOM)); 304 } 305 306 @Override 307 public boolean onTouchEvent(android.view.MotionEvent event) { 308 if (mDrawIdentifier.equals(FOLDER_PORTRAIT) || 309 mDrawIdentifier.equals(FOLDER_LANDSCAPE) || 310 mDrawIdentifier.equals(FOLDER_LARGE)) { 311 Folder f = mLauncher.getWorkspace().getOpenFolder(); 312 if (f != null) { 313 Rect r = new Rect(); 314 f.getHitRect(r); 315 if (r.contains((int) event.getX(), (int) event.getY())) { 316 return false; 317 } 318 } 319 } 320 return super.onTouchEvent(event); 321 }; 322 323 @Override 324 public boolean onTouch(View v, MotionEvent ev) { 325 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 326 mTouchDownPt[0] = (int) ev.getX(); 327 mTouchDownPt[1] = (int) ev.getY(); 328 } 329 return false; 330 } 331 332 @Override 333 public void onClick(View v) { 334 if (mDrawIdentifier.equals(WORKSPACE_PORTRAIT) || 335 mDrawIdentifier.equals(WORKSPACE_LANDSCAPE) || 336 mDrawIdentifier.equals(WORKSPACE_LARGE)) { 337 if (mFocusedHotseatAppBounds != null && 338 mFocusedHotseatAppBounds.contains(mTouchDownPt[0], mTouchDownPt[1])) { 339 // Launch the activity that is being highlighted 340 Intent intent = new Intent(Intent.ACTION_MAIN); 341 intent.setComponent(mFocusedHotseatAppComponent); 342 intent.addCategory(Intent.CATEGORY_LAUNCHER); 343 mLauncher.startActivity(intent, null); 344 mLauncher.dismissWorkspaceCling(this); 345 } 346 } 347 } 348 349 @Override 350 public boolean onLongClick(View v) { 351 if (mDrawIdentifier.equals(WORKSPACE_PORTRAIT) || 352 mDrawIdentifier.equals(WORKSPACE_LANDSCAPE) || 353 mDrawIdentifier.equals(WORKSPACE_LARGE)) { 354 mLauncher.dismissWorkspaceCling(null); 355 return true; 356 } 357 return false; 358 } 359 360 @Override 361 protected void dispatchDraw(Canvas canvas) { 362 if (mIsInitialized) { 363 canvas.save(); 364 365 // Get the background override if there is one 366 if (mBackground == null) { 367 if (mDrawIdentifier.equals(WORKSPACE_CUSTOM)) { 368 mBackground = getResources().getDrawable(R.drawable.bg_cling5); 369 } 370 } 371 // Draw the background 372 Bitmap eraseBg = null; 373 Canvas eraseCanvas = null; 374 if (mScrimView != null) { 375 // Skip drawing the background 376 mScrimView.setBackgroundColor(mBackgroundColor); 377 } else if (mBackground != null) { 378 mBackground.setBounds(0, 0, getMeasuredWidth(), getMeasuredHeight()); 379 mBackground.draw(canvas); 380 } else if (mDrawIdentifier.equals(WORKSPACE_PORTRAIT) || 381 mDrawIdentifier.equals(WORKSPACE_LANDSCAPE) || 382 mDrawIdentifier.equals(WORKSPACE_LARGE)) { 383 // Initialize the draw buffer (to allow punching through) 384 eraseBg = Bitmap.createBitmap(getMeasuredWidth(), getMeasuredHeight(), 385 Bitmap.Config.ARGB_8888); 386 eraseCanvas = new Canvas(eraseBg); 387 eraseCanvas.drawColor(mBackgroundColor); 388 } else { 389 canvas.drawColor(mBackgroundColor); 390 } 391 392 // Draw everything else 393 DisplayMetrics metrics = new DisplayMetrics(); 394 mLauncher.getWindowManager().getDefaultDisplay().getMetrics(metrics); 395 float alpha = getAlpha(); 396 View content = getContent(); 397 if (content != null) { 398 alpha *= content.getAlpha(); 399 } 400 if (mDrawIdentifier.equals(FIRST_RUN_PORTRAIT) || 401 mDrawIdentifier.equals(FIRST_RUN_LANDSCAPE)) { 402 // Draw the circle 403 View bubbleContent = findViewById(R.id.bubble_content); 404 Rect bubbleRect = new Rect(); 405 bubbleContent.getGlobalVisibleRect(bubbleRect); 406 mBubblePaint.setAlpha((int) (255 * alpha)); 407 float buffer = DynamicGrid.pxFromDp(FIRST_RUN_CIRCLE_BUFFER_DPS, metrics); 408 canvas.drawCircle(metrics.widthPixels / 2, 409 bubbleRect.centerY(), 410 (bubbleContent.getMeasuredWidth() + buffer) / 2, 411 mBubblePaint); 412 } else if (mDrawIdentifier.equals(WORKSPACE_PORTRAIT) || 413 mDrawIdentifier.equals(WORKSPACE_LANDSCAPE) || 414 mDrawIdentifier.equals(WORKSPACE_LARGE)) { 415 int offset = DynamicGrid.pxFromDp(WORKSPACE_CIRCLE_Y_OFFSET_DPS, metrics); 416 mErasePaint.setAlpha((int) (128)); 417 eraseCanvas.drawCircle(metrics.widthPixels / 2, 418 metrics.heightPixels / 2 - offset, 419 DynamicGrid.pxFromDp(WORKSPACE_OUTER_CIRCLE_RADIUS_DPS, metrics), 420 mErasePaint); 421 mErasePaint.setAlpha(0); 422 eraseCanvas.drawCircle(metrics.widthPixels / 2, 423 metrics.heightPixels / 2 - offset, 424 DynamicGrid.pxFromDp(WORKSPACE_INNER_CIRCLE_RADIUS_DPS, metrics), 425 mErasePaint); 426 canvas.drawBitmap(eraseBg, 0, 0, null); 427 eraseCanvas.setBitmap(null); 428 eraseBg = null; 429 430 // Draw the focused hotseat app icon 431 if (mFocusedHotseatAppBounds != null && mFocusedHotseatApp != null) { 432 mFocusedHotseatApp.setBounds(mFocusedHotseatAppBounds.left, 433 mFocusedHotseatAppBounds.top, mFocusedHotseatAppBounds.right, 434 mFocusedHotseatAppBounds.bottom); 435 mFocusedHotseatApp.setAlpha((int) (255 * alpha)); 436 mFocusedHotseatApp.draw(canvas); 437 } 438 } 439 440 canvas.restore(); 441 } 442 443 // Draw the rest of the cling 444 super.dispatchDraw(canvas); 445 }; 446 } 447