summaryrefslogtreecommitdiffstats
path: root/src/org/cyanogenmod/theme/perapptheming/PerAppThemingWindow.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/org/cyanogenmod/theme/perapptheming/PerAppThemingWindow.java')
-rw-r--r--src/org/cyanogenmod/theme/perapptheming/PerAppThemingWindow.java1078
1 files changed, 1078 insertions, 0 deletions
diff --git a/src/org/cyanogenmod/theme/perapptheming/PerAppThemingWindow.java b/src/org/cyanogenmod/theme/perapptheming/PerAppThemingWindow.java
new file mode 100644
index 0000000..27db329
--- /dev/null
+++ b/src/org/cyanogenmod/theme/perapptheming/PerAppThemingWindow.java
@@ -0,0 +1,1078 @@
+/*
+ * Copyright (C) 2016 Cyanogen, Inc.
+ * Copyright (C) 2016 The CyanogenMod Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.cyanogenmod.theme.perapptheming;
+
+import android.animation.Animator;
+import android.animation.ValueAnimator;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.ThemeConfig;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.graphics.PixelFormat;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.IBinder;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.WindowManager;
+import android.view.animation.Interpolator;
+import android.view.animation.OvershootInterpolator;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.cyanogenmod.theme.chooser.R;
+import org.cyanogenmod.theme.util.Utils;
+
+import cyanogenmod.providers.ThemesContract.ThemesColumns;
+import cyanogenmod.themes.ThemeChangeRequest;
+import cyanogenmod.themes.ThemeManager;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+
+public class PerAppThemingWindow extends Service implements OnTouchListener,
+ ThemeManager.ThemeChangeListener {
+ // Animation frame rate per second
+ private static final int ANIMATION_FRAME_RATE = 60;
+
+ private static final int EXIT_DELETE_MODE_ANIMATION_DURATION = 50;
+
+ private static final int MOVE_TO_DELETE_BOX_ANIMATION_DURATION = 150;
+
+ private static final int ANIMATION_DURATION = 300;
+
+ private static final int FAB_SCALE_ANIMATION_DURATION = 150;
+
+ private static final int LIST_ON_LEFT_SIDE = 0;
+ private static final int LIST_ON_RIGHT_SIDE = 1;
+
+ // Don't want these colors to be themable and possibly alter the effect we are after, so
+ // they are defined here rather than in colors.xml
+ private static final int SCRIM_COLOR_TRANSPARENT = 0x00000000;
+ private static final int SCRIM_COLOR_OPAQUE = 0xaa000000;
+
+ // Amount to wait after a theme change occurred before fading the scrim away
+ // This value was obtained empirically by performing theme changes and adjusting this delay
+ private static final int THEME_CHANGE_DELAY = 1500;
+
+ private static final float PRESSED_FAB_SCALE = 0.95f;
+
+ private static final float DELETE_BOX_ANIMATION_SCALE = 0.3f;
+
+ private static final int MAX_DEPRECIATION = 5;
+
+ private static final float FAB_ANIMATION_SCALE_FACTOR = 0.44f;
+
+ // Margin around the phone
+ private static int MARGIN_VERTICAL;
+ // Margin around the phone
+ private static int MARGIN_HORIZONTAL;
+ private static int CLOSE_ANIMATION_DISTANCE;
+ private static int DRAG_DELTA;
+ private static int STARTING_POINT_Y;
+ private static int DELETE_BOX_WIDTH;
+ private static int DELETE_BOX_HEIGHT;
+ private static int FLOATING_WINDOW_ICON_SIZE;
+
+ // View variables
+ private BroadcastReceiver mBroadcastReceiver;
+ private WindowManager mWindowManager;
+ private LinearLayout mDraggableIcon;
+ private View mDraggableIconImage;
+ private WindowManager.LayoutParams mParams;
+ private PerAppThemeListLayout mThemeListLayout;
+ private WindowManager.LayoutParams mListLayoutParams;
+ private ListView mThemeList;
+ private ThemesAdapter mAdapter;
+ private FrameLayout.LayoutParams mListParams;
+ private LinearLayout mDeleteView;
+ private View mDeleteBoxView;
+ private View mThemeApplyingView;
+ private boolean mDeleteBoxVisible = false;
+ private boolean mIsDestroyed = false;
+ private boolean mIsBeingDestroyed = false;
+ private int mCurrentPosX = -1;
+
+ // Animation variables
+ private List<Float> mDeltaXArray;
+ private List<Float> mDeltaYArray;
+ private AnimationTask mAnimationTask;
+
+ // Close logic
+ private int mCurrentX;
+ private int mCurrentY;
+ private boolean mIsInDeleteMode = false;
+ private boolean mIsAnimationLocked = false;
+
+ // Drag variables
+ float mPrevDragX;
+ float mPrevDragY;
+ float mOrigX;
+ float mOrigY;
+ boolean mDragged;
+
+ private int mListSide = LIST_ON_LEFT_SIDE;
+
+ private ThemeConfig mThemeConfig;
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ // Load margins, distances, etc.
+ final Resources res = getResources();
+ MARGIN_VERTICAL =
+ res.getDimensionPixelSize(R.dimen.floating_window_margin_vertical);
+ MARGIN_HORIZONTAL =
+ res.getDimensionPixelSize(R.dimen.floating_window_margin_horizontal);
+ CLOSE_ANIMATION_DISTANCE =
+ res.getDimensionPixelSize(R.dimen.floating_window_close_animation_distance);
+ DRAG_DELTA = res.getDimensionPixelSize(R.dimen.floating_window_drag_delta);
+ STARTING_POINT_Y = res.getDimensionPixelSize(R.dimen.floating_window_starting_point_y);
+
+ DELETE_BOX_WIDTH = (int) getResources().getDimension(
+ R.dimen.floating_window_delete_box_width);
+ DELETE_BOX_HEIGHT = (int) getResources().getDimension(
+ R.dimen.floating_window_delete_box_height);
+ FLOATING_WINDOW_ICON_SIZE = (int) getResources().getDimension(
+ R.dimen.floating_window_icon);
+
+ mDeleteView = new LinearLayout(getContext());
+ View.inflate(getContext(), R.layout.per_app_delete_box_window, mDeleteView);
+ mDeleteBoxView = mDeleteView.findViewById(R.id.box);
+ addView(mDeleteView, 0, 0, Gravity.BOTTOM | Gravity.CENTER_VERTICAL,
+ WindowManager.LayoutParams.MATCH_PARENT,
+ WindowManager.LayoutParams.WRAP_CONTENT);
+ mDeleteView.setVisibility(View.GONE);
+
+ mDraggableIcon = new LinearLayout(this);
+ mDraggableIcon.setOnTouchListener(this);
+ View.inflate(getContext(), R.layout.per_app_fab_floating_window_icon, mDraggableIcon);
+ mDraggableIconImage = mDraggableIcon.findViewById(R.id.box);
+ mDraggableIconImage.setClipToOutline(true);
+ mDraggableIconImage.getViewTreeObserver().addOnWindowAttachListener(mWindowAttachListener);
+ mParams = addView(mDraggableIcon, 0, 0);
+ updateIconPosition(MARGIN_HORIZONTAL, STARTING_POINT_Y);
+
+ mThemeListLayout = (PerAppThemeListLayout) View.inflate(getContext(),
+ R.layout.per_app_theme_list, null);
+ mThemeListLayout.setPerAppThemingWindow(this);
+ mThemeList = (ListView) mThemeListLayout.findViewById(R.id.theme_list);
+ mListParams = (FrameLayout.LayoutParams) mThemeList.getLayoutParams();
+ mThemeApplyingView = mThemeListLayout.findViewById(R.id.applying_theme_text);
+
+ final Configuration config = getResources().getConfiguration();
+ mThemeConfig = getThemeConfig(config);
+ loadThemes();
+ getContentResolver().registerContentObserver(ThemesColumns.CONTENT_URI, true,
+ mThemesObserver);
+ }
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (mIsBeingDestroyed) return true;
+
+ switch(event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ if (mThemeListLayout.isAttachedToWindow()) {
+ hideThemeList();
+ return false;
+ }
+ mPrevDragX = mOrigX = event.getRawX();
+ mPrevDragY = mOrigY = event.getRawY();
+
+ mDragged = false;
+
+ mDeltaXArray = new LinkedList<Float>();
+ mDeltaYArray = new LinkedList<Float>();
+
+ mCurrentX = mParams.x;
+ mCurrentY = mParams.y;
+
+ mDraggableIconImage.setScaleX(PRESSED_FAB_SCALE);
+ mDraggableIconImage.setScaleY(PRESSED_FAB_SCALE);
+
+ // Cancel any currently running animations
+ if (mAnimationTask != null) {
+ mAnimationTask.cancel();
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ mIsAnimationLocked = false;
+ if (mAnimationTask != null) {
+ mAnimationTask.cancel();
+ }
+
+ if (!mDragged) {
+ // clicked so show theme list
+ final int mid = getScreenWidth() / 2;
+ mListSide = LIST_ON_LEFT_SIDE;
+ if (mCurrentPosX > mid) mListSide = LIST_ON_RIGHT_SIDE;
+ if (!mThemeListLayout.isAttachedToWindow()) showThemeList();
+ } else {
+ // Animate the icon
+ mAnimationTask = new AnimationTask();
+ mAnimationTask.run();
+ }
+
+ if (mIsInDeleteMode) {
+ close(true);
+ } else {
+ hideDeleteBox();
+ mDraggableIconImage.setScaleX(1f);
+ mDraggableIconImage.setScaleY(1f);
+ }
+ break;
+ case MotionEvent.ACTION_MOVE:
+ mCurrentX = (int) (event.getRawX() - mDraggableIcon.getWidth() / 2);
+ mCurrentY = (int) (event.getRawY() - mDraggableIcon.getHeight());
+ if (isDeleteMode()) {
+ mDeleteBoxView.setBackgroundResource(R.drawable.btn_quicktheme_remove_hover);
+ mIsInDeleteMode = true;
+ updateIconPosition(mCurrentX, mCurrentY);
+ } else if (mIsInDeleteMode){
+ mDeleteBoxView.setBackgroundResource(R.drawable.btn_quicktheme_remove_normal);
+ mIsInDeleteMode = false;
+ } else {
+ if(!mIsAnimationLocked && mDragged) {
+ if (mAnimationTask != null) {
+ mAnimationTask.cancel();
+ }
+ updateIconPosition(mCurrentX, mCurrentY);
+ }
+ }
+
+ float deltaX = event.getRawX() - mPrevDragX;
+ float deltaY = event.getRawY() - mPrevDragY;
+
+ mDeltaXArray.add(deltaX);
+ mDeltaYArray.add(deltaY);
+
+ mPrevDragX = event.getRawX();
+ mPrevDragY = event.getRawY();
+
+ deltaX = event.getRawX() - mOrigX;
+ deltaY = event.getRawY() - mOrigY;
+ mDragged = mDragged || Math.abs(deltaX) > DRAG_DELTA
+ || Math.abs(deltaY) > DRAG_DELTA;
+ if (mDragged) {
+ showDeleteBox();
+ }
+ break;
+ }
+
+ return true;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mIsDestroyed = true;
+ if (mDraggableIcon != null) {
+ removeViewIfAttached(mDraggableIcon);
+ mDraggableIcon = null;
+ }
+ if (mDeleteView != null) {
+ removeViewIfAttached(mDeleteView);
+ mDeleteView = null;
+ }
+ if (mThemeListLayout != null) {
+ removeViewIfAttached(mThemeListLayout);
+ mThemeListLayout = null;
+ }
+ if (mAnimationTask != null) {
+ mAnimationTask.cancel();
+ mAnimationTask = null;
+ }
+ if (mBroadcastReceiver != null) {
+ unregisterReceiver(mBroadcastReceiver);
+ mBroadcastReceiver = null;
+ }
+ if (mThemesObserver != null) {
+ getContentResolver().unregisterContentObserver(mThemesObserver);
+ mThemesObserver = null;
+ }
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mThemeConfig = getThemeConfig(newConfig);
+ }
+
+ @Override
+ public void onProgress(int progress) {
+ }
+
+ @Override
+ public void onFinish(boolean isSuccess) {
+ ThemeManager tm = ThemeManager.getInstance(getContext());
+ tm.removeClient(this);
+ mThemeListLayout.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ hideScrim();
+ startFabScaleUpAnimation();
+ }
+ }, THEME_CHANGE_DELAY);
+ }
+
+ public void hideThemeList() {
+ hideThemeList(false, new Runnable() {
+ @Override
+ public void run() {
+ removeViewIfAttached(mThemeListLayout);
+ }
+ });
+ }
+
+ private ThemeConfig getThemeConfig(Configuration config) {
+ if (config != null && config.themeConfig != null) {
+ return config.themeConfig;
+ }
+
+ return ThemeConfig.getBootTheme(getContentResolver());
+ }
+
+ private void removeViewIfAttached(View view) {
+ if (view.isAttachedToWindow()) {
+ mWindowManager.removeViewImmediate(view);
+ }
+ }
+
+ private WindowManager.LayoutParams addView(View v, int x, int y) {
+ return addView(v, x, y, Gravity.TOP | Gravity.LEFT,
+ WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT);
+ }
+
+ private WindowManager.LayoutParams addView(View v, int x, int y, int gravity,
+ int width, int height) {
+ mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE);
+ WindowManager.LayoutParams params = new WindowManager.LayoutParams(width, height,
+ WindowManager.LayoutParams.TYPE_SYSTEM_ALERT,
+ WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSLUCENT);
+
+ params.gravity = gravity;
+ params.x = x;
+ params.y = y;
+
+ mWindowManager.addView(v, params);
+
+ return params;
+ }
+
+ private void updateIconPosition(int x, int y) {
+ mCurrentPosX = x;
+
+ View v = mDraggableIconImage;
+ v.setTranslationX(0);
+ if (x < 0) {
+ v.setTranslationX(x);
+ x = 0;
+ }
+
+ if (x > getScreenWidth() - FLOATING_WINDOW_ICON_SIZE) {
+ v.setTranslationX(x - getScreenWidth() + FLOATING_WINDOW_ICON_SIZE);
+ x = getScreenWidth() - FLOATING_WINDOW_ICON_SIZE;
+ }
+
+ v.setTranslationY(0);
+ if (y < 0) {
+ v.setTranslationY(y);
+ y = 0;
+ }
+
+ if (y > getScreenHeight() - FLOATING_WINDOW_ICON_SIZE) {
+ v.setTranslationY(y - getScreenHeight() + FLOATING_WINDOW_ICON_SIZE);
+ y = getScreenHeight() - FLOATING_WINDOW_ICON_SIZE;
+ }
+ mParams.x = x;
+ mParams.y = y;
+
+ if (!mIsDestroyed) {
+ mWindowManager.updateViewLayout(mDraggableIcon, mParams);
+ }
+ }
+
+ private boolean isDeleteMode() {
+ return isHoveringOverDeleteBox(mParams.y);
+ }
+
+ private boolean isHoveringOverDeleteBox(int y) {
+ return y + mDraggableIconImage.getHeight() >= getScreenHeight() - DELETE_BOX_HEIGHT;
+ }
+
+ private void showDeleteBox() {
+ if (!mDeleteBoxVisible) {
+ mDeleteBoxVisible = true;
+ mDeleteView.setVisibility(View.VISIBLE);
+
+ mDeleteBoxView.setAlpha(0);
+ mDeleteBoxView.setTranslationY(CLOSE_ANIMATION_DISTANCE);
+ mDeleteBoxView.animate().alpha(1).translationYBy(-1 * CLOSE_ANIMATION_DISTANCE)
+ .setListener(null);
+
+ mDeleteBoxView.getLayoutParams().width = getScreenWidth();
+ }
+ }
+
+ private void hideDeleteBox() {
+ if (mDeleteBoxVisible) {
+ mDeleteBoxVisible = false;
+ if (mDeleteView != null) {
+ mDeleteBoxView.animate().alpha(0)
+ .translationYBy(CLOSE_ANIMATION_DISTANCE)
+ .setListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mDeleteView != null) mDeleteView.setVisibility(View.GONE);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+ });
+ }
+ }
+ }
+
+ private void animateToDeleteBoxCenter(final OnAnimationFinishedListener l) {
+ if (mIsAnimationLocked) {
+ return;
+ }
+ mIsInDeleteMode = true;
+
+ if (mAnimationTask != null) {
+ mAnimationTask.cancel();
+ }
+
+ mAnimationTask = new AnimationTask(getScreenWidth() / 2 - mDraggableIcon.getWidth() / 2,
+ getScreenHeight() - DELETE_BOX_HEIGHT / 2 - mDraggableIcon.getHeight() / 2);
+ mAnimationTask.setDuration(MOVE_TO_DELETE_BOX_ANIMATION_DURATION);
+ mAnimationTask.setAnimationFinishedListener(l);
+ mAnimationTask.run();
+ mDeleteBoxView.setBackgroundResource(R.drawable.btn_quicktheme_remove_hover);
+ }
+
+ private void close(boolean animate) {
+ if (mIsBeingDestroyed) {
+ return;
+ }
+ mIsBeingDestroyed = true;
+
+ if (animate) {
+ animateToDeleteBoxCenter(new OnAnimationFinishedListener() {
+ @Override
+ public void onAnimationFinished() {
+ hideDeleteBox();
+ mDeleteBoxView.animate()
+ .scaleX(DELETE_BOX_ANIMATION_SCALE)
+ .scaleY(DELETE_BOX_ANIMATION_SCALE);
+ mDraggableIconImage.animate()
+ .scaleX(DELETE_BOX_ANIMATION_SCALE)
+ .scaleY(DELETE_BOX_ANIMATION_SCALE)
+ .translationY(CLOSE_ANIMATION_DISTANCE)
+ .setDuration(mDeleteBoxView.animate().getDuration())
+ .setListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ stopSelf();
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+ });
+ }
+ });
+ } else {
+ stopSelf();
+ }
+ }
+
+ private static interface OnAnimationFinishedListener {
+ public void onAnimationFinished();
+ }
+
+ private Context getContext() {
+ return this;
+ }
+
+ private int getScreenWidth() {
+ return getResources().getDisplayMetrics().widthPixels;
+ }
+
+ private int getScreenHeight() {
+ return getResources().getDisplayMetrics().heightPixels - getStatusBarHeight();
+ }
+
+ private int getStatusBarHeight() {
+ int result = 0;
+ int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
+ if (resourceId > 0) {
+ result = getResources().getDimensionPixelSize(resourceId);
+ }
+
+ return result;
+ }
+
+ private void loadThemes() {
+ String[] columns = {ThemesColumns._ID, ThemesColumns.TITLE, ThemesColumns.PKG_NAME};
+ String selection = ThemesColumns.MODIFIES_OVERLAYS + "=? AND " +
+ ThemesColumns.INSTALL_STATE + "=?";
+ String[] selectionArgs = {"1", "" + ThemesColumns.InstallState.INSTALLED};
+ String sortOrder = ThemesColumns.TITLE + " ASC";
+ Cursor c = getContentResolver().query(ThemesColumns.CONTENT_URI, columns, selection,
+ selectionArgs, sortOrder);
+ if (c != null) {
+ if (mAdapter == null) {
+ mAdapter = new ThemesAdapter(this, c);
+ mThemeList.setAdapter(mAdapter);
+ mThemeList.setOnItemClickListener(mThemeClickedListener);
+ } else {
+ String pkgName = (String) mAdapter.getItem(0);
+ mAdapter.populateThemes(c);
+ mAdapter.setCurrentTheme(pkgName);
+ }
+ }
+ }
+
+ private ContentObserver mThemesObserver = new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ onChange(selfChange, null);
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ loadThemes();
+ }
+ };
+
+ private ViewTreeObserver.OnWindowAttachListener mWindowAttachListener =
+ new ViewTreeObserver.OnWindowAttachListener() {
+ @Override
+ public void onWindowAttached() {
+ // Remove the this OnWindowAttachListener now that we are done with it.
+ mDraggableIconImage.getViewTreeObserver().removeOnWindowAttachListener(this);
+
+ final float fabWidth = getResources().getDimension(R.dimen.floating_window_icon);
+ mDraggableIconImage.setAlpha(0);
+ mDraggableIconImage.setX(-fabWidth);
+ mDraggableIconImage.animate()
+ .alpha(1f)
+ .xBy(fabWidth)
+ .setDuration(ANIMATION_DURATION)
+ .start();
+ }
+
+ @Override
+ public void onWindowDetached() {
+ }
+ };
+
+ private void showThemeList() {
+ if (mListLayoutParams == null) {
+ mListLayoutParams = new WindowManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ WindowManager.LayoutParams.TYPE_PHONE,
+ WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH |
+ WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
+ WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED |
+ WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
+ PixelFormat.TRANSLUCENT);
+ }
+ mListLayoutParams.gravity = Gravity.TOP |
+ (mListSide == LIST_ON_LEFT_SIDE ? Gravity.LEFT : Gravity.RIGHT);
+ mWindowManager.addView(mThemeListLayout, mListLayoutParams);
+
+ setThemeListPosition();
+ startFabScaleDownAnimation();
+
+ mAdapter.setCurrentTheme(
+ mThemeConfig.getOverlayPkgNameForApp(Utils.getTopTaskPackageName(this)));
+ mThemeListLayout.circularReveal(mParams.x + mDraggableIconImage.getWidth() / 2,
+ mParams.y + mDraggableIconImage.getHeight() / 2, ANIMATION_DURATION);
+ }
+
+ private void hideThemeList(boolean showScrim, final Runnable endAction) {
+ if (showScrim) {
+ showScrim();
+ } else {
+ startFabScaleUpAnimation();
+ }
+ mThemeListLayout.circularHide(mParams.x + mDraggableIconImage.getWidth() / 2,
+ mParams.y + mDraggableIconImage.getHeight() / 2, ANIMATION_DURATION);
+ if (endAction != null) {
+ mDraggableIcon.postDelayed(endAction, ANIMATION_DURATION);
+ }
+ }
+
+ private void showScrim() {
+ ValueAnimator animator = ValueAnimator.ofArgb(SCRIM_COLOR_TRANSPARENT,
+ SCRIM_COLOR_OPAQUE);
+ mThemeListLayout.setEnabled(false);
+ animator.setDuration(ANIMATION_DURATION)
+ .addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ Integer value = (Integer) animation.getAnimatedValue();
+ mThemeListLayout.setBackgroundColor(value.intValue());
+ }
+ });
+ animator.start();
+ mThemeApplyingView.animate()
+ .alpha(1f)
+ .setDuration(ANIMATION_DURATION);
+ }
+
+ private void hideScrim() {
+ ValueAnimator animator = ValueAnimator.ofArgb(SCRIM_COLOR_OPAQUE, SCRIM_COLOR_TRANSPARENT);
+ animator.setDuration(ANIMATION_DURATION)
+ .addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ Integer value = (Integer) animation.getAnimatedValue();
+ mThemeListLayout.setBackgroundColor(value.intValue());
+ }
+ });
+ animator.addListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ removeViewIfAttached(mThemeListLayout);
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animation) {
+ }
+ });
+ animator.start();
+ mThemeApplyingView.animate()
+ .alpha(0f)
+ .setDuration(ANIMATION_DURATION);
+ mDraggableIcon.setVisibility(View.VISIBLE);
+ mDraggableIconImage.animate()
+ .alpha(1f)
+ .setDuration(ANIMATION_DURATION);
+ }
+
+ private void setThemeListPosition() {
+ int thirdHeight = getScreenHeight() / 3;
+ // use the center of the fab to decide where to place the list
+ int fabLocationY = mParams.y + mDraggableIconImage.getHeight() / 2;
+ int listHeight = mThemeList.getMeasuredHeight();
+ if (listHeight <= 0) {
+ // not measured yet so let's force that
+ int width = getResources().getDimensionPixelSize(R.dimen.theme_list_width);
+ int height = getResources().getDimensionPixelSize(R.dimen.theme_list_max_height);
+ mThemeList.measure(View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.AT_MOST),
+ View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.AT_MOST));
+ listHeight = mThemeList.getMeasuredHeight();
+ }
+
+ // If we're in the top 1/3 of the screen position the top of the list with the top
+ // of the fab. Second 3rd will position the list so that it is vertically centered
+ // with the fab center. Bottom 3rd will position the bottom of the list with the
+ // bottom of the fab.
+ if (fabLocationY < thirdHeight) {
+ mListParams.topMargin = mParams.y + mDraggableIconImage.getHeight() / 2;
+ } else if (fabLocationY < thirdHeight * 2) {
+ mListParams.topMargin = fabLocationY - listHeight / 2;
+ } else {
+ mListParams.topMargin = mParams.y + mDraggableIconImage.getHeight() / 2 - listHeight;
+ }
+ mListParams.gravity = Gravity.TOP |
+ (mListSide == LIST_ON_LEFT_SIDE ? Gravity.LEFT : Gravity.RIGHT);
+ mThemeList.setLayoutParams(mListParams);
+ }
+
+ private void startFabScaleDownAnimation() {
+ final int iconWidth = mDraggableIconImage.getWidth();
+ final float translateX = (iconWidth - (float) iconWidth * FAB_ANIMATION_SCALE_FACTOR) / 2 *
+ (mListSide == LIST_ON_LEFT_SIDE ? -1 : 1);
+
+ mDraggableIconImage.animate()
+ .scaleX(FAB_ANIMATION_SCALE_FACTOR)
+ .scaleY(FAB_ANIMATION_SCALE_FACTOR)
+ .translationXBy(translateX)
+ .setDuration(FAB_SCALE_ANIMATION_DURATION);
+ }
+
+ private void startFabScaleUpAnimation() {
+ final float iconWidth = mDraggableIconImage.getWidth();
+ final float translateX = (iconWidth - (float) iconWidth * FAB_ANIMATION_SCALE_FACTOR) / 2 *
+ (mListSide == LIST_ON_LEFT_SIDE ? 1 : -1);
+
+ mDraggableIconImage.animate()
+ .scaleX(1f)
+ .scaleY(1f)
+ .translationXBy(translateX)
+ .setDuration(FAB_SCALE_ANIMATION_DURATION);
+ }
+
+ private AdapterView.OnItemClickListener mThemeClickedListener =
+ new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
+ final String themePkgName = (String) view.getTag(R.id.tag_key_name);
+ final String appPkgName = Utils.getTopTaskPackageName(getContext());
+ if (!TextUtils.isEmpty(appPkgName) && !TextUtils.isEmpty(themePkgName)) {
+ if (!Utils.themeHasOverlayForApp(getContext(), appPkgName, themePkgName)) {
+ Toast.makeText(getContext(), R.string.per_app_theme_app_not_overlaid_warning,
+ Toast.LENGTH_LONG).show();
+ }
+ hideThemeList(true, new Runnable() {
+ @Override
+ public void run() {
+ ThemeManager tm = ThemeManager.getInstance(getContext());
+ ThemeChangeRequest.Builder builder = new ThemeChangeRequest.Builder();
+ builder.setAppOverlay(appPkgName, themePkgName);
+ try {
+ tm.addClient(PerAppThemingWindow.this);
+ } catch (IllegalArgumentException e) {
+ /* ignore since this means we already have a listener added */
+ }
+ tm.requestThemeChange(builder.build(), false);
+ }
+ });
+ } else {
+ hideThemeList();
+ }
+ }
+ };
+
+ private float calculateVelocityX() {
+ int depreciation = mDeltaXArray.size() + 1;
+ float sum = 0;
+ for (Float f : mDeltaXArray) {
+ depreciation--;
+ if (depreciation > MAX_DEPRECIATION){
+ continue;
+ }
+
+ sum += f / depreciation;
+ }
+
+ return sum;
+ }
+
+ private float calculateVelocityY() {
+ int depreciation = mDeltaYArray.size() + 1;
+ float sum = 0;
+ for (Float f : mDeltaYArray) {
+ depreciation--;
+ if (depreciation > 5) {
+ continue;
+ }
+
+ sum += f / depreciation;
+ }
+
+ return sum;
+ }
+
+ // Timer for animation/automatic movement of the tray
+ private class AnimationTask {
+ // Ultimate destination coordinates toward which the view will move
+ int mDestX;
+ int mDestY;
+ long mDuration = 350;
+ long mStartTime;
+ float mTension = 1.4f;
+ Interpolator mInterpolator = new OvershootInterpolator(mTension);
+ long mSteps;
+ long mCurrentStep;
+ int mDistX;
+ int mOrigX;
+ int mDistY;
+ int mOrigY;
+ Handler mAnimationHandler = new Handler();
+ OnAnimationFinishedListener mAnimationFinishedListener;
+
+ public AnimationTask(int x, int y) {
+ setup(x, y);
+ }
+
+ public AnimationTask() {
+ setup(calculateX(), calculateY());
+
+ float velocityX = calculateVelocityX();
+ float velocityY = calculateVelocityY();
+ mTension += Math.sqrt(velocityX * velocityX + velocityY * velocityY) / 200;
+ mInterpolator = new OvershootInterpolator(mTension);
+ }
+
+ private void setup(int x, int y) {
+ if (mIsAnimationLocked) {
+ throw new RuntimeException("Returning to user's finger. Avoid animations while " +
+ "mIsAnimationLocked flag is set.");
+ }
+
+ mDestX = x;
+ mDestY = y;
+
+ mSteps = (int) (((float) mDuration) / 1000 * ANIMATION_FRAME_RATE);
+ mCurrentStep = 1;
+ mDistX = mParams.x - mDestX;
+ mOrigX = mParams.x;
+ mDistY = mParams.y - mDestY;
+ mOrigY = mParams.y;
+ }
+
+ public long getDuration() {
+ return mDuration;
+ }
+
+ public void setDuration(long duration) {
+ mDuration = duration;
+ setup(mDestX, mDestY);
+ }
+
+ public OnAnimationFinishedListener getAnimationFinishedListener() {
+ return mAnimationFinishedListener;
+ }
+
+ public void setAnimationFinishedListener(OnAnimationFinishedListener l) {
+ mAnimationFinishedListener = l;
+ }
+
+ public Interpolator getInterpolator() {
+ return mInterpolator;
+ }
+
+ public void setInterpolator(Interpolator interpolator) {
+ mInterpolator = interpolator;
+ }
+
+ private int calculateX() {
+ float velocityX = calculateVelocityX();
+ int screenWidth = getScreenWidth();
+ int destX = (mParams.x + mDraggableIcon.getWidth() / 2 > screenWidth / 2)
+ ? screenWidth - mDraggableIcon.getWidth() - MARGIN_HORIZONTAL
+ : 0 + MARGIN_HORIZONTAL;
+
+ if (Math.abs(velocityX) > 50) {
+ destX = (velocityX > 0) ? screenWidth - mDraggableIcon.getWidth()
+ - MARGIN_HORIZONTAL : 0 + MARGIN_HORIZONTAL;
+ }
+
+ return destX;
+ }
+
+ private int calculateY() {
+ float velocityY = calculateVelocityY();
+ mInterpolator = new OvershootInterpolator(mTension);
+ int screenHeight = getScreenHeight();
+ int destY = mParams.y + (int) (velocityY * 3);
+ if (destY <= 0) {
+ destY = MARGIN_VERTICAL;
+ }
+ if (destY >= screenHeight - mDraggableIcon.getHeight()) {
+ destY = screenHeight - mDraggableIcon.getHeight() - MARGIN_VERTICAL;
+ }
+
+ return destY;
+ }
+
+ public void run() {
+ mStartTime = System.currentTimeMillis();
+ for (mCurrentStep = 1; mCurrentStep <= mSteps; mCurrentStep++) {
+ long delay = mCurrentStep * mDuration / mSteps;
+ final float currentStep = mCurrentStep;
+ mAnimationHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ // Update coordinates of the view
+ float percent = mInterpolator.getInterpolation(currentStep / mSteps);
+ updateIconPosition(mOrigX - (int) (percent * mDistX), mOrigY
+ - (int) (percent * mDistY));
+
+ // Notify the animation has ended
+ if (currentStep >= mSteps) {
+ if (mAnimationFinishedListener != null) mAnimationFinishedListener
+ .onAnimationFinished();
+ }
+ }
+ }, delay);
+ }
+ }
+
+ public void cancel() {
+ mAnimationHandler.removeCallbacksAndMessages(null);
+ mAnimationTask = null;
+ }
+ }
+
+ /**
+ * We're extending BaseAdapter rather than CursorAdapter so that we can quickly re-order
+ * the list without needing to requery the provider. We're only storing the package name
+ * and theme title so there is minimum memory impact on doing this.
+ */
+ class ThemesAdapter extends BaseAdapter {
+ private static final float HALF_OPACITY = 0.5f;
+ private static final float FULL_OPACITY = 1.0f;
+
+ private ArrayList<ThemeInfo> mThemes;
+ private LayoutInflater mInflater;
+
+ public ThemesAdapter(Context context, Cursor cursor) {
+ mInflater = (LayoutInflater) context.getSystemService(LAYOUT_INFLATER_SERVICE);
+ mThemes = new ArrayList<ThemeInfo>(cursor.getCount());
+ populateThemes(cursor);
+ cursor.close();
+ }
+
+ @Override
+ public int getCount() {
+ return mThemes.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mThemes.get(position).pkgName;
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return 0;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = mInflater.inflate(R.layout.per_app_theme_list_item, parent, false);
+ Holder holder = new Holder();
+ holder.title = (TextView) convertView.findViewById(R.id.theme_title);
+ holder.indicator = (TextView) convertView.findViewById(R.id.selected_indicator);
+ convertView.setTag(R.id.tag_key_holder, holder);
+ }
+ ThemeInfo themeInfo = mThemes.get(position);
+ Holder holder = (Holder) convertView.getTag(R.id.tag_key_holder);
+ holder.title.setText(themeInfo.title);
+ if (position == 0) {
+ holder.title.setAlpha(HALF_OPACITY);
+ holder.indicator.setVisibility(View.VISIBLE);
+ convertView.setEnabled(false);
+ } else {
+ holder.title.setAlpha(FULL_OPACITY);
+ holder.indicator.setVisibility(View.INVISIBLE);
+ convertView.setEnabled(true);
+ }
+ convertView.setTag(R.id.tag_key_name, themeInfo.pkgName);
+ return convertView;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return position != 0;
+ }
+
+ public void setCurrentTheme(String pkgName) {
+ ThemeInfo info = null;
+ for (ThemeInfo ti : mThemes) {
+ if (ti.pkgName.equals(pkgName)) {
+ info = ti;
+ break;
+ }
+ }
+ if (info != null) {
+ Collections.sort(mThemes);
+ mThemes.remove(info);
+ mThemes.add(0, info);
+ notifyDataSetChanged();
+ }
+ }
+
+ private void populateThemes(Cursor cursor) {
+ mThemes.clear();
+ while(cursor.moveToNext()) {
+ ThemeInfo info = new ThemeInfo(
+ cursor.getString(cursor.getColumnIndex(ThemesColumns.PKG_NAME)),
+ cursor.getString(cursor.getColumnIndex(ThemesColumns.TITLE)));
+ mThemes.add(info);
+ }
+ }
+
+ private class Holder {
+ TextView title;
+ TextView indicator;
+ }
+
+ private class ThemeInfo implements Comparable {
+ String pkgName;
+ String title;
+
+ public ThemeInfo(String pkgName, String title) {
+ this.pkgName = pkgName;
+ this.title = title;
+ }
+
+ @Override
+ public int compareTo(Object another) {
+ return this.title.compareTo(((ThemeInfo)another).title);
+ }
+ }
+ }
+}