1/* 2 * Copyright (C) 2016 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.car.cluster.sample.cards; 18 19import android.content.Context; 20import android.graphics.Bitmap; 21import android.graphics.Bitmap.Config; 22import android.graphics.Canvas; 23import android.graphics.Color; 24import android.graphics.Paint; 25import android.graphics.Paint.Style; 26import android.graphics.PorterDuff.Mode; 27import android.graphics.PorterDuffXfermode; 28import android.os.Handler; 29import android.os.Looper; 30import android.os.SystemClock; 31import android.util.AttributeSet; 32import android.util.Log; 33import android.view.ViewGroup; 34import android.view.animation.DecelerateInterpolator; 35import android.widget.FrameLayout; 36import android.widget.ImageView; 37import android.widget.ImageView.ScaleType; 38import android.widget.ViewSwitcher; 39 40import com.android.car.cluster.sample.DebugUtil; 41import com.android.car.cluster.sample.R; 42 43/** 44 * View that responsible for displaying cards in instrument cluster (including media, phone and 45 * maps). 46 */ 47public class CardView extends FrameLayout implements Comparable<CardView> { 48 49 private final static String TAG = DebugUtil.getTag(CardView.class); 50 51 protected final static long SHOW_ANIMATION_DURATION = 1000 * DebugUtil.ANIMATION_FACTOR; 52 53 protected ImageView mBackgroundImage; 54 protected ViewGroup mDetailsPanel; 55 protected ViewSwitcher mLeftIconSwitcher; 56 protected ViewSwitcher mRightIconSwitcher; 57 58 private Bitmap mBitmap = Bitmap.createBitmap(1, 1, Config.ARGB_8888); 59 60 protected long mLastUpdated = SystemClock.elapsedRealtime(); 61 62 private Canvas mCanvas = new Canvas(); 63 private final Paint mBackgroundCirclePaint; 64 65 private final static Handler mHandler = new Handler(Looper.getMainLooper()); 66 67 protected final int mCardWidth; 68 protected final int mCardHeight; 69 protected final int mCurveRadius; 70 protected final int mTextPanelWidth; 71 protected final float mLeftPadding; 72 protected final float mIconSize; 73 protected final float mIconsOverlap; 74 75 protected int mPriority = 9; 76 77 protected final static int PRIORITY_GARBAGE = 10; 78 79 protected final static int PRIORITY_CALL_INCOMING = 3; 80 protected final static int PRIORITY_CALL_ACTIVE = 5; 81 protected final static int PRIORITY_MEDIA_NOTIFICATION = 3; 82 protected final static int PRIORITY_MEDIA_ACTIVE = 6; 83 protected final static int PRIORITY_WEATHER_CARD = 9; 84 protected final static int PRIORITY_NAVIGATION_ACTIVE = 3; 85 protected final static int PRIORITY_HANGOUT_NOTIFICATION = 3; 86 87 @CardType 88 private int mCardType; 89 private final PriorityChangedListener mPriorityChangedListener; 90 91 public @interface CardType { 92 int WEATHER = 1; 93 int MEDIA = 2; 94 int PHONE_CALL = 3; 95 int NAV = 4; 96 int HANGOUT = 5; 97 } 98 99 public CardView(Context context, @CardType int cardType, PriorityChangedListener listener) { 100 this(context, null, cardType, listener); 101 } 102 103 public CardView(Context context, AttributeSet attrs, @CardType int cardType, 104 PriorityChangedListener listener) { 105 super(context, attrs); 106 mPriorityChangedListener = listener; 107 if (DebugUtil.DEBUG) { 108 Log.d(TAG, "ctor"); 109 } 110 mCardType = cardType; 111 112 setWillNotDraw(false); // This will trigger onDraw method. 113 114 mBackgroundCirclePaint = createBackgroundCirclePaint(); 115 mCardWidth = (int)getResources().getDimension(R.dimen.card_width); 116 mCardHeight = (int) getResources().getDimension(R.dimen.card_height); 117 mTextPanelWidth = (int)getResources().getDimension(R.dimen.card_message_panel_width); 118 mLeftPadding = getResources().getDimension(R.dimen.card_content_left_padding); 119 mIconSize = getResources().getDimension(R.dimen.card_icon_size); 120 mIconsOverlap = mIconSize - mLeftPadding; 121 mCurveRadius = (int)(mCardWidth * 0.643f); 122 123 inflate(getContext(), R.layout.card_view, this); 124 125 if (this.isInEditMode()) { 126 return; 127 } 128 129 mLeftIconSwitcher = viewById(R.id.left_icon_switcher); 130 mRightIconSwitcher = viewById(R.id.right_icon_switcher); 131 mBackgroundImage = viewById(R.id.image_background); 132 133 init(); 134 } 135 136 protected void inflate(int layoutId) { 137 inflate(getContext(), layoutId, (ViewGroup) getChildAt(0)); 138 } 139 140 protected void init() { 141 } 142 143 @CardType 144 public int getCardType() { 145 return mCardType; 146 } 147 148 public void setLeftIcon(Bitmap bitmap) { 149 setLeftIcon(bitmap, false /* animated */); 150 } 151 152 public void setLeftIcon(Bitmap bitmap, boolean animated) { 153 if (DebugUtil.DEBUG) { 154 Log.d(TAG, "setLeftIcon, bitmap: " + bitmap); 155 } 156 switchImageViewBitmpa(bitmap, mLeftIconSwitcher, animated); 157 } 158 159 160 public void setRightIcon(Bitmap bitmap) { 161 setRightIcon(bitmap, false /* animated */); 162 } 163 164 /** 165 * @param bitmap if null, the image won't be displayed and message panel will be placed 166 * accordingly. 167 */ 168 public void setRightIcon(Bitmap bitmap, boolean animated) { 169 if (DebugUtil.DEBUG) { 170 Log.d(TAG, "setRightIcon, bitmap: " + bitmap); 171 } 172 if (bitmap == null && mRightIconSwitcher.getVisibility() == VISIBLE) { 173 mRightIconSwitcher.setVisibility(GONE); 174 } else if (bitmap != null && mRightIconSwitcher.getVisibility() == GONE) { 175 mRightIconSwitcher.setVisibility(VISIBLE); 176 } 177 178 switchImageViewBitmpa(bitmap, mRightIconSwitcher, animated); 179 } 180 181 private void switchImageViewBitmpa(Bitmap bitmap, ViewSwitcher switcher, boolean animated) { 182 ImageView icon = (ImageView) (animated 183 ? switcher.getNextView() : switcher.getCurrentView()); 184 185 icon.setBackground(null); 186 icon.setImageBitmap(bitmap); 187 188 if (animated) { 189 switcher.showNext(); 190 } 191 } 192 193 /** Called by {@code ClusterView} when card should go away using unreveal animation */ 194 public void onPlayUnrevealAnimation() { 195 if (DebugUtil.DEBUG) { 196 Log.d(TAG, "onPlayUnrevealAnimation"); 197 } 198 } 199 200 public void onPlayRevealAnimation() { 201 if (DebugUtil.DEBUG) { 202 Log.d(TAG, "onPlayRevealAnimation"); 203 } 204 205 if (mLeftIconSwitcher.getVisibility() == VISIBLE) { 206 mLeftIconSwitcher.setTranslationX(mCardWidth / 2); 207 mLeftIconSwitcher.animate() 208 .translationX(getLeftIconTargetX()) 209 .setDuration(SHOW_ANIMATION_DURATION) 210 .setInterpolator(getDecelerateInterpolator()); 211 } 212 213 if (mRightIconSwitcher.getVisibility() == VISIBLE) { 214 mRightIconSwitcher.setTranslationX( mCardWidth - mTextPanelWidth / 2 - mIconSize); 215 mRightIconSwitcher.animate() 216 .translationX(getRightIconTargetX()) 217 .setDuration(SHOW_ANIMATION_DURATION) 218 .setInterpolator(getDecelerateInterpolator()); 219 } 220 221 showDetailsPanelAnimation(getDetailsPanelTargetX()); 222 } 223 224 protected float getLeftIconTargetX() { 225 return mLeftIconSwitcher.getVisibility() == VISIBLE ? mLeftPadding : 0; 226 } 227 228 protected float getRightIconTargetX() { 229 if (mRightIconSwitcher.getVisibility() != VISIBLE) { 230 return 0; 231 } 232 float x = mLeftPadding; 233 if (mLeftIconSwitcher.getVisibility() == VISIBLE) { 234 x += mIconsOverlap; 235 } 236 return x; 237 } 238 239 protected float getDetailsPanelTargetX() { 240 return Math.max(getLeftIconTargetX(), getRightIconTargetX()) + mIconSize + mLeftPadding; 241 } 242 243 protected void showDetailsPanelAnimation(float textX) { 244 if (mDetailsPanel != null) { 245 mDetailsPanel.setTranslationX(mCardWidth - mTextPanelWidth / 2); 246 mDetailsPanel.animate() 247 .translationX(textX) 248 .setDuration(SHOW_ANIMATION_DURATION) 249 .setInterpolator(getDecelerateInterpolator()); 250 } 251 } 252 253 protected DecelerateInterpolator getDecelerateInterpolator() { 254 return new DecelerateInterpolator(2f); 255 } 256 257 public void setBackground(Bitmap bmpPicture, int backgroundColor) { 258 Bitmap bmpBackground = Bitmap.createBitmap( 259 mCardWidth, 260 (int)getResources().getDimension(R.dimen.card_height), 261 Config.ARGB_8888); 262 Canvas canvas = new Canvas(bmpBackground); 263 //clear previous drawings 264 canvas.drawColor(Color.TRANSPARENT, Mode.CLEAR); 265 266 Paint p = new Paint(); 267 p.setColor(backgroundColor); 268 p.setAntiAlias(true); 269 p.setStyle(Style.FILL); 270 // Draw curved background. 271 canvas.drawCircle(mCurveRadius, (int)getResources().getDimension( 272 R.dimen.card_height) / 2, 273 mCurveRadius, p); 274 275 // Draw image respecting curved background. 276 p.setXfermode(new PorterDuffXfermode(Mode.SRC_IN)); 277 float x = canvas.getWidth() - bmpPicture.getWidth(); 278 float y = canvas.getHeight() - bmpPicture.getHeight(); 279 if (y < 0) { 280 y = y / 2; // Center image if it is not fitting. 281 } 282 canvas.drawBitmap(bmpPicture, x, y, p); 283 284 mBackgroundImage.setScaleType(ScaleType.CENTER_CROP); 285 mBackgroundImage.setImageBitmap(bmpBackground); 286 if (mBackgroundImage.getVisibility() != VISIBLE) { 287 mBackgroundImage.setVisibility(VISIBLE); 288 } 289 } 290 291 public void setBackgroundColor(int color) { 292 mBackgroundCirclePaint.setColor(color); 293 } 294 295 private Paint createBackgroundCirclePaint() { 296 Paint p = new Paint(); 297 p.setAntiAlias(true); 298 p.setXfermode(new PorterDuffXfermode(Mode.ADD)); 299 p.setStyle(Style.FILL); 300 p.setColor(getResources().getColor(R.color.cluster_active_area_background, null)); 301 return p; 302 } 303 304 @Override 305 protected void onDraw(Canvas canvas) { 306 super.onDraw(canvas); 307 308 if(mBitmap.isRecycled() || mBitmap.getWidth() != canvas.getWidth() 309 || mBitmap.getHeight() != canvas.getHeight()) { 310 Log.d(TAG, "creating bitmap..."); 311 mBitmap.recycle(); 312 mBitmap = Bitmap.createBitmap(canvas.getWidth(), canvas.getHeight(), Config.ARGB_8888); 313 mCanvas.setBitmap(mBitmap); 314 } 315 316 //clear previous drawings 317 mCanvas.drawColor(Color.TRANSPARENT, Mode.CLEAR); 318 319 mCanvas.drawCircle(mCurveRadius, getHeight() / 2, mCurveRadius, mBackgroundCirclePaint); 320 321 canvas.drawBitmap(mBitmap, 0, 0, null); 322 } 323 324 public int getIconSize() { 325 return (int)mIconSize; 326 } 327 328 public static void runDelayed(long delay, final Runnable task) { 329 mHandler.postDelayed(task, delay); 330 } 331 332 public void setPriority(int priority) { 333 mPriority = priority; 334 mPriorityChangedListener.onPriorityChanged(this, priority); 335 } 336 337 /** 338 * Should return number from 0 to 9. 0 - is most important, 9 is less important. 339 */ 340 public int getPriority() { 341 return mPriority; 342 } 343 344 public boolean isGarbage() { 345 return getPriority() == PRIORITY_GARBAGE; 346 } 347 348 public void removeGracefully() { 349 setPriority(PRIORITY_GARBAGE); 350 } 351 352 @Override 353 public int compareTo(CardView another) { 354 int res = this.getPriority() - another.getPriority(); 355 if (res == 0) { 356 // If objects have the same priorities, check the last time they were updated. 357 res = this.mLastUpdated > another.mLastUpdated ? -1 : 1; 358 if (DebugUtil.DEBUG) { 359 Log.d(TAG, "Found card with the same priority: " + this + " and " + another + "," 360 + "this.mLastUpdated: " + mLastUpdated 361 + ", another.mLastUpdated:" + another.mLastUpdated + ", res: " + res); 362 363 } 364 } 365 return res; 366 } 367 368 @SuppressWarnings("unchecked") 369 protected <E> E viewById(int id) { 370 return (E) findViewById(id); 371 } 372 373 public interface PriorityChangedListener { 374 void onPriorityChanged(CardView cardView, int newPriority); 375 } 376} 377