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
17package com.android.launcher3;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.app.ActivityOptions;
22import android.content.Context;
23import android.content.ComponentName;
24import android.content.Intent;
25import android.content.res.Resources;
26import android.content.res.TypedArray;
27import android.graphics.*;
28import android.graphics.drawable.Drawable;
29import android.util.AttributeSet;
30import android.util.DisplayMetrics;
31import android.view.FocusFinder;
32import android.view.MotionEvent;
33import android.view.View;
34import android.view.animation.AccelerateInterpolator;
35import android.widget.FrameLayout;
36import android.widget.TextView;
37
38public 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