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