Repository: vikramkakkar/DeckView Branch: master Commit: 0487563749a8 Files: 74 Total size: 247.1 KB Directory structure: gitextract_kqwh3f90/ ├── .gitignore ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── appeaser/ │ │ └── deckviewlibrary/ │ │ └── ApplicationTest.java │ └── main/ │ ├── AndroidManifest.xml │ └── res/ │ └── values/ │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── deckview/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── appeaser/ │ │ └── deckview/ │ │ └── ApplicationTest.java │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── appeaser/ │ │ └── deckview/ │ │ ├── helpers/ │ │ │ ├── DeckChildViewTransform.java │ │ │ ├── DeckViewConfig.java │ │ │ ├── DeckViewSwipeHelper.java │ │ │ └── FakeShadowDrawable.java │ │ ├── utilities/ │ │ │ ├── DVConstants.java │ │ │ ├── DVUtils.java │ │ │ ├── DozeTrigger.java │ │ │ └── ReferenceCountedTrigger.java │ │ └── views/ │ │ ├── AnimateableDeckChildViewBounds.java │ │ ├── DeckChildView.java │ │ ├── DeckChildViewHeader.java │ │ ├── DeckChildViewThumbnail.java │ │ ├── DeckView.java │ │ ├── DeckViewLayoutAlgorithm.java │ │ ├── DeckViewScroller.java │ │ ├── DeckViewTouchHandler.java │ │ ├── FixedSizeImageView.java │ │ ├── ViewAnimation.java │ │ └── ViewPool.java │ └── res/ │ ├── drawable-v21/ │ │ ├── deck_child_view_button_bg.xml │ │ ├── deck_child_view_dismiss_dark.xml │ │ ├── deck_child_view_dismiss_light.xml │ │ ├── deck_child_view_header_bg.xml │ │ └── deck_child_view_header_bg_color.xml │ ├── interpolator-v21/ │ │ ├── decelerate_quint.xml │ │ ├── fast_out_linear_in.xml │ │ ├── fast_out_slow_in.xml │ │ └── linear_out_slow_in.xml │ ├── layout/ │ │ ├── deck_child_view.xml │ │ └── deck_child_view_header.xml │ ├── values/ │ │ ├── colors.xml │ │ ├── config.xml │ │ ├── dimens.xml │ │ └── strings.xml │ ├── values-land/ │ │ └── dimens.xml │ ├── values-sw600dp/ │ │ └── dimens.xml │ ├── values-sw600dp-land/ │ │ └── dimens.xml │ └── values-sw720dp/ │ └── dimens.xml ├── deckviewsample/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── appeaser/ │ │ └── deckviewsample/ │ │ └── ApplicationTest.java │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── appeaser/ │ │ └── deckviewsample/ │ │ ├── Datum.java │ │ └── DeckViewSampleActivity.java │ └── res/ │ ├── drawable-v21/ │ │ └── box.xml │ ├── layout/ │ │ └── activity_deck_view_sample.xml │ ├── menu/ │ │ └── menu_deck_view_sample.xml │ ├── values/ │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── styles.xml │ ├── values-v21/ │ │ └── styles.xml │ └── values-w820dp/ │ └── dimens.xml ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ ################# ## AndroidStudio ################# .gradle project.properties .idea gen *.class out *.iml ################# ## Eclipse ################# *.pydevproject .project .metadata bin/ tmp/ *.tmp *.bak *.swp *~.nib local.properties .classpath .settings/ .loadpath # External tool builders .externalToolBuilders/ # Locally stored "Eclipse launch configurations" *.launch # CDT-specific .cproject # PDT-specific .buildpath ################# ## Visual Studio ################# ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. # User-specific files *.suo *.user *.sln.docstates # Build results [Dd]ebug/ [Rr]elease/ x64/ build/ [Bb]in/ [Oo]bj/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* *_i.c *_p.c *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.log *.scc # Visual C++ cache files ipch/ *.aps *.ncb *.opensdf *.sdf *.cachefile # Visual Studio profiler *.psess *.vsp *.vspx # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # NCrunch *.ncrunch* .*crunch*.local.xml # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.Publish.xml *.pubxml # NuGet Packages Directory ## TODO: If you have NuGet Package Restore enabled, uncomment the next line #packages/ # Windows Azure Build Output csx *.build.csdef # Windows Store app package directory AppPackages/ # Others sql/ *.Cache ClientBin/ [Ss]tyle[Cc]op.* ~$* *~ *.dbmdl *.[Pp]ublish.xml *.pfx *.publishsettings # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file to a newer # Visual Studio version. Backup files are not needed, because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm # SQL Server files App_Data/*.mdf App_Data/*.ldf ############# ## Windows detritus ############# # Windows image file caches Thumbs.db ehthumbs.db # Folder config file Desktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Mac crap .DS_Store ############# ## Python ############# *.py[co] # Packages *.egg *.egg-info dist/ build/ eggs/ parts/ var/ sdist/ develop-eggs/ .installed.cfg # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox #Translations *.mo #Mr Developer .mr.developer.cfg ================================================ FILE: README.md ================================================ # DeckView A ViewGroup that mimics Android (Lollipop) Recent apps screen layout. ######Note: DeckView is **not** a true recycler. It *does* recycle views - but it also updates progress map for *all* of its children on each scroll step. This will result in lags with large datasets. ================================================ FILE: app/.gitignore ================================================ /build ================================================ FILE: app/build.gradle ================================================ apply plugin: 'com.android.application' android { compileSdkVersion 22 buildToolsVersion "22.0.1" defaultConfig { applicationId "com.appeaser.deckviewlibrary" minSdkVersion 21 targetSdkVersion 22 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in C:\Users\Vikram\Documents\Android\adt-bundle-windows-x86-20130219\sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: app/src/androidTest/java/com/appeaser/deckviewlibrary/ApplicationTest.java ================================================ package com.appeaser.deckviewlibrary; import android.app.Application; import android.test.ApplicationTestCase; /** * Testing Fundamentals */ public class ApplicationTest extends ApplicationTestCase { public ApplicationTest() { super(Application.class); } } ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/res/values/strings.xml ================================================ DeckViewLibrary ================================================ FILE: app/src/main/res/values/styles.xml ================================================ ================================================ FILE: build.gradle ================================================ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:1.1.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { jcenter() } } ================================================ FILE: deckview/.gitignore ================================================ /build ================================================ FILE: deckview/build.gradle ================================================ apply plugin: 'com.android.library' android { compileSdkVersion 22 buildToolsVersion "22.0.1" defaultConfig { minSdkVersion 21 targetSdkVersion 22 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) } ================================================ FILE: deckview/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in C:/Users/Vikram/Documents/Android/adt-bundle-windows-x86-20130219/sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: deckview/src/androidTest/java/com/appeaser/deckview/ApplicationTest.java ================================================ package com.appeaser.deckview; import android.app.Application; import android.test.ApplicationTestCase; /** * Testing Fundamentals */ public class ApplicationTest extends ApplicationTestCase { public ApplicationTest() { super(Application.class); } } ================================================ FILE: deckview/src/main/AndroidManifest.xml ================================================ ================================================ FILE: deckview/src/main/java/com/appeaser/deckview/helpers/DeckChildViewTransform.java ================================================ package com.appeaser.deckview.helpers; import android.animation.ValueAnimator; import android.graphics.Rect; import android.view.View; import android.view.ViewPropertyAnimator; import android.view.animation.Interpolator; /** * Created by Vikram on 02/04/2015. */ /* The transform state for a task view */ public class DeckChildViewTransform { public int startDelay = 0; public int translationY = 0; public float translationZ = 0; public float scale = 1f; public float alpha = 1f; public boolean visible = false; public Rect rect = new Rect(); public float p = 0f; public DeckChildViewTransform() { // Do nothing } public DeckChildViewTransform(DeckChildViewTransform o) { startDelay = o.startDelay; translationY = o.translationY; translationZ = o.translationZ; scale = o.scale; alpha = o.alpha; visible = o.visible; rect.set(o.rect); p = o.p; } /** * Resets the current transform */ public void reset() { startDelay = 0; translationY = 0; translationZ = 0; scale = 1f; alpha = 1f; visible = false; rect.setEmpty(); p = 0f; } /** * Convenience functions to compare against current property values */ public boolean hasAlphaChangedFrom(float v) { return (Float.compare(alpha, v) != 0); } public boolean hasScaleChangedFrom(float v) { return (Float.compare(scale, v) != 0); } public boolean hasTranslationYChangedFrom(float v) { return (Float.compare(translationY, v) != 0); } public boolean hasTranslationZChangedFrom(float v) { return (Float.compare(translationZ, v) != 0); } /** * Applies this transform to a view. */ public void applyToTaskView(View v, int duration, Interpolator interp, boolean allowLayers, boolean allowShadows, ValueAnimator.AnimatorUpdateListener updateCallback) { // Check to see if any properties have changed, and update the task view if (duration > 0) { ViewPropertyAnimator anim = v.animate(); boolean requiresLayers = false; // Animate to the final state if (hasTranslationYChangedFrom(v.getTranslationY())) { anim.translationY(translationY); } if (allowShadows && hasTranslationZChangedFrom(v.getTranslationZ())) { anim.translationZ(translationZ); } if (hasScaleChangedFrom(v.getScaleX())) { anim.scaleX(scale) .scaleY(scale); requiresLayers = true; } if (hasAlphaChangedFrom(v.getAlpha())) { // Use layers if we animate alpha anim.alpha(alpha); requiresLayers = true; } if (requiresLayers && allowLayers) { anim.withLayer(); } if (updateCallback != null) { anim.setUpdateListener(updateCallback); } else { anim.setUpdateListener(null); } anim.setStartDelay(startDelay) .setDuration(duration) .setInterpolator(interp) .start(); } else { // Set the changed properties if (hasTranslationYChangedFrom(v.getTranslationY())) { v.setTranslationY(translationY); } if (allowShadows && hasTranslationZChangedFrom(v.getTranslationZ())) { v.setTranslationZ(translationZ); } if (hasScaleChangedFrom(v.getScaleX())) { v.setScaleX(scale); v.setScaleY(scale); } if (hasAlphaChangedFrom(v.getAlpha())) { v.setAlpha(alpha); } } } /** * Reset the transform on a view. */ public static void reset(View v) { v.setTranslationX(0f); v.setTranslationY(0f); v.setTranslationZ(0f); v.setScaleX(1f); v.setScaleY(1f); v.setAlpha(1f); } @Override public String toString() { return "TaskViewTransform delay: " + startDelay + " y: " + translationY + " z: " + translationZ + " scale: " + scale + " alpha: " + alpha + " visible: " + visible + " rect: " + rect + " p: " + p; } } ================================================ FILE: deckview/src/main/java/com/appeaser/deckview/helpers/DeckViewConfig.java ================================================ package com.appeaser.deckview.helpers; /** * Created by Vikram on 02/04/2015. */ import android.content.Context; import android.content.SharedPreferences; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Rect; import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.animation.AnimationUtils; import android.view.animation.Interpolator; import com.appeaser.deckview.R; import com.appeaser.deckview.utilities.DVConstants; /** * Configuration helper */ public class DeckViewConfig { static DeckViewConfig sInstance; static int sPrevConfigurationHashCode; /** * Levels of svelte in increasing severity/austerity. */ // No svelting. public static final int SVELTE_NONE = 0; // Limit thumbnail cache to number of visible thumbnails when Recents was loaded, disable // caching thumbnails as you scroll. public static final int SVELTE_LIMIT_CACHE = 1; // Disable the thumbnail cache, load thumbnails asynchronously when the activity loads and // evict all thumbnails when hidden. public static final int SVELTE_DISABLE_CACHE = 2; // Disable all thumbnail loading. public static final int SVELTE_DISABLE_LOADING = 3; /** * Animations */ public float animationPxMovementPerSecond; /** * Interpolators */ public Interpolator fastOutSlowInInterpolator; public Interpolator fastOutLinearInInterpolator; public Interpolator linearOutSlowInInterpolator; public Interpolator quintOutInterpolator; /** * Filtering */ public int filteringCurrentViewsAnimDuration; public int filteringNewViewsAnimDuration; /** * Insets */ public Rect systemInsets = new Rect(); public Rect displayRect = new Rect(); /** * Layout */ boolean isLandscape; /** * Task stack */ public int taskStackScrollDuration; public int taskStackMaxDim; public int taskStackTopPaddingPx; public float taskStackWidthPaddingPct; public float taskStackOverscrollPct; /** * Transitions */ public int transitionEnterFromAppDelay; public int transitionEnterFromHomeDelay; /** * Task view animation and styles */ public int taskViewEnterFromAppDuration; public int taskViewEnterFromHomeDuration; public int taskViewEnterFromHomeStaggerDelay; public int taskViewExitToAppDuration; public int taskViewExitToHomeDuration; public int taskViewRemoveAnimDuration; public int taskViewRemoveAnimTranslationXPx; public int taskViewTranslationZMinPx; public int taskViewTranslationZMaxPx; public int taskViewRoundedCornerRadiusPx; public int taskViewHighlightPx; public int taskViewAffiliateGroupEnterOffsetPx; public float taskViewThumbnailAlpha; /** * Task bar colors */ public int taskBarViewDefaultBackgroundColor; public int taskBarViewLightTextColor; public int taskBarViewDarkTextColor; public int taskBarViewHighlightColor; public float taskBarViewAffiliationColorMinAlpha; /** * Task bar size & animations */ public int taskBarHeight; public int taskBarDismissDozeDelaySeconds; /** * Nav bar scrim */ public int navBarScrimEnterDuration; /** * Launch states */ public boolean launchedWithAltTab; public boolean launchedWithNoRecentTasks; public boolean launchedFromAppWithThumbnail; public boolean launchedFromHome; public boolean launchedFromSearchHome; public boolean launchedReuseTaskStackViews; public boolean launchedHasConfigurationChanged; public int launchedToTaskId; public int launchedNumVisibleTasks; public int launchedNumVisibleThumbnails; /** * Misc * */ public boolean useHardwareLayers; public int altTabKeyDelay; public boolean fakeShadows; /** * Dev options and global settings */ public boolean debugModeEnabled; public int svelteLevel; /** * Private constructor */ private DeckViewConfig(Context context) { // Properties that don't have to be reloaded with each configuration change can be loaded // here. // Interpolators fastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context, R.interpolator.fast_out_slow_in); fastOutLinearInInterpolator = AnimationUtils.loadInterpolator(context, R.interpolator.fast_out_linear_in); linearOutSlowInInterpolator = AnimationUtils.loadInterpolator(context, R.interpolator.linear_out_slow_in); quintOutInterpolator = AnimationUtils.loadInterpolator(context, R.interpolator.decelerate_quint); } /** * Updates the configuration to the current context */ public static DeckViewConfig reinitialize(Context context) { if (sInstance == null) { sInstance = new DeckViewConfig(context); } int configHashCode = context.getResources().getConfiguration().hashCode(); if (sPrevConfigurationHashCode != configHashCode) { sInstance.update(context); sPrevConfigurationHashCode = configHashCode; } sInstance.updateOnReinitialize(context); return sInstance; } /** * Returns the current recents configuration */ public static DeckViewConfig getInstance() { return sInstance; } /** * Updates the state, given the specified context */ void update(Context context) { SharedPreferences settings = context.getSharedPreferences(context.getPackageName(), 0); Resources res = context.getResources(); DisplayMetrics dm = res.getDisplayMetrics(); // Debug mode debugModeEnabled = settings.getBoolean(DVConstants.Values.App.Key_DebugModeEnabled, false); // Layout isLandscape = res.getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; // Insets displayRect.set(0, 0, dm.widthPixels, dm.heightPixels); // Animations animationPxMovementPerSecond = res.getDimensionPixelSize(R.dimen.animation_movement_in_dps_per_second); // Filtering filteringCurrentViewsAnimDuration = res.getInteger(R.integer.filter_animate_current_views_duration); filteringNewViewsAnimDuration = res.getInteger(R.integer.filter_animate_new_views_duration); // Task stack taskStackScrollDuration = res.getInteger(R.integer.animate_deck_scroll_duration); TypedValue widthPaddingPctValue = new TypedValue(); res.getValue(R.dimen.deck_width_padding_percentage, widthPaddingPctValue, true); taskStackWidthPaddingPct = widthPaddingPctValue.getFloat(); TypedValue stackOverscrollPctValue = new TypedValue(); res.getValue(R.dimen.deck_overscroll_percentage, stackOverscrollPctValue, true); taskStackOverscrollPct = stackOverscrollPctValue.getFloat(); taskStackMaxDim = res.getInteger(R.integer.max_deck_view_dim); taskStackTopPaddingPx = res.getDimensionPixelSize(R.dimen.deck_top_padding); // Transition transitionEnterFromAppDelay = res.getInteger(R.integer.enter_from_app_transition_duration); transitionEnterFromHomeDelay = res.getInteger(R.integer.enter_from_home_transition_duration); // Task view animation and styles taskViewEnterFromAppDuration = res.getInteger(R.integer.task_enter_from_app_duration); taskViewEnterFromHomeDuration = res.getInteger(R.integer.task_enter_from_home_duration); taskViewEnterFromHomeStaggerDelay = res.getInteger(R.integer.task_enter_from_home_stagger_delay); taskViewExitToAppDuration = res.getInteger(R.integer.task_exit_to_app_duration); taskViewExitToHomeDuration = res.getInteger(R.integer.task_exit_to_home_duration); taskViewRemoveAnimDuration = res.getInteger(R.integer.animate_task_view_remove_duration); taskViewRemoveAnimTranslationXPx = res.getDimensionPixelSize(R.dimen.task_view_remove_anim_translation_x); taskViewRoundedCornerRadiusPx = res.getDimensionPixelSize(R.dimen.task_view_rounded_corners_radius); taskViewHighlightPx = res.getDimensionPixelSize(R.dimen.task_view_highlight); taskViewTranslationZMinPx = res.getDimensionPixelSize(R.dimen.task_view_z_min); taskViewTranslationZMaxPx = res.getDimensionPixelSize(R.dimen.task_view_z_max); taskViewAffiliateGroupEnterOffsetPx = res.getDimensionPixelSize(R.dimen.task_view_affiliate_group_enter_offset); TypedValue thumbnailAlphaValue = new TypedValue(); res.getValue(R.dimen.task_view_thumbnail_alpha, thumbnailAlphaValue, true); taskViewThumbnailAlpha = thumbnailAlphaValue.getFloat(); // Task bar colors taskBarViewDefaultBackgroundColor = res.getColor(R.color.task_bar_default_background_color); taskBarViewLightTextColor = res.getColor(R.color.task_bar_light_text_color); taskBarViewDarkTextColor = res.getColor(R.color.task_bar_dark_text_color); taskBarViewHighlightColor = res.getColor(R.color.task_bar_highlight_color); TypedValue affMinAlphaPctValue = new TypedValue(); res.getValue(R.dimen.task_affiliation_color_min_alpha_percentage, affMinAlphaPctValue, true); taskBarViewAffiliationColorMinAlpha = affMinAlphaPctValue.getFloat(); // Task bar size & animations taskBarHeight = res.getDimensionPixelSize(R.dimen.deck_child_header_bar_height); taskBarDismissDozeDelaySeconds = res.getInteger(R.integer.task_bar_dismiss_delay_seconds); // Nav bar scrim navBarScrimEnterDuration = res.getInteger(R.integer.nav_bar_scrim_enter_duration); // Misc useHardwareLayers = res.getBoolean(R.bool.config_use_hardware_layers); altTabKeyDelay = res.getInteger(R.integer.deck_alt_tab_key_delay); fakeShadows = res.getBoolean(R.bool.config_fake_shadows); svelteLevel = res.getInteger(R.integer.deck_svelte_level); } /** * Updates the system insets */ public void updateSystemInsets(Rect insets) { systemInsets.set(insets); } /** * Updates the search bar app widget */ public void updateSearchBarAppWidgetId(Context context, int appWidgetId) { } /** * Updates the states that need to be re-read whenever we re-initialize. */ void updateOnReinitialize(Context context/*, SystemServicesProxy ssp*/) { } /** * Called when the configuration has changed, and we want to reset any configuration specific * members. */ public void updateOnConfigurationChange() { // Reset this flag on configuration change to ensure that we recreate new task views launchedReuseTaskStackViews = false; // Set this flag to indicate that the configuration has changed since Recents last launched launchedHasConfigurationChanged = true; } /** * Returns the task stack bounds in the current orientation. These bounds do not account for * the system insets. */ public void getTaskStackBounds(int windowWidth, int windowHeight, int topInset, int rightInset, Rect taskStackBounds) { taskStackBounds.set(0, 0, windowWidth, windowHeight); } } ================================================ FILE: deckview/src/main/java/com/appeaser/deckview/helpers/DeckViewSwipeHelper.java ================================================ package com.appeaser.deckview.helpers; /** * Created by Vikram on 02/04/2015. */ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.os.Build; import android.util.DisplayMetrics; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.animation.LinearInterpolator; /** * This class facilitates swipe to dismiss. It defines an interface to be implemented by the * by the class hosting the views that need to swiped, and, using this interface, handles touch * events and translates / fades / animates the view as it is dismissed. */ public class DeckViewSwipeHelper { static final String TAG = "DeckViewSwipeHelper"; private static final boolean SLOW_ANIMATIONS = false; // DEBUG; private static final boolean CONSTRAIN_SWIPE = true; private static final boolean FADE_OUT_DURING_SWIPE = true; private static final boolean DISMISS_IF_SWIPED_FAR_ENOUGH = true; public static final int X = 0; public static final int Y = 1; private static LinearInterpolator sLinearInterpolator = new LinearInterpolator(); private float SWIPE_ESCAPE_VELOCITY = 100f; // dp/sec private int DEFAULT_ESCAPE_ANIMATION_DURATION = 75; // ms private int MAX_ESCAPE_ANIMATION_DURATION = 150; // ms private static final int SNAP_ANIM_LEN = SLOW_ANIMATIONS ? 1000 : 250; // ms public static float ALPHA_FADE_START = 0.15f; // fraction of thumbnail width // where fade starts static final float ALPHA_FADE_END = 0.65f; // fraction of thumbnail width // beyond which alpha->0 private float mMinAlpha = 0f; private float mPagingTouchSlop; Callback mCallback; private int mSwipeDirection; private VelocityTracker mVelocityTracker; private float mInitialTouchPos; private boolean mDragging; private View mCurrView; private boolean mCanCurrViewBeDimissed; private float mDensityScale; public boolean mAllowSwipeTowardsStart = true; public boolean mAllowSwipeTowardsEnd = true; private boolean mRtl; public DeckViewSwipeHelper(int swipeDirection, Callback callback, float densityScale, float pagingTouchSlop) { mCallback = callback; mSwipeDirection = swipeDirection; mVelocityTracker = VelocityTracker.obtain(); mDensityScale = densityScale; mPagingTouchSlop = pagingTouchSlop; } public void setDensityScale(float densityScale) { mDensityScale = densityScale; } public void setPagingTouchSlop(float pagingTouchSlop) { mPagingTouchSlop = pagingTouchSlop; } public void cancelOngoingDrag() { if (mDragging) { if (mCurrView != null) { mCallback.onDragCancelled(mCurrView); setTranslation(mCurrView, 0); mCallback.onSnapBackCompleted(mCurrView); mCurrView = null; } mDragging = false; } } public void resetTranslation(View v) { setTranslation(v, 0); } private float getPos(MotionEvent ev) { return mSwipeDirection == X ? ev.getX() : ev.getY(); } private float getTranslation(View v) { return mSwipeDirection == X ? v.getTranslationX() : v.getTranslationY(); } private float getVelocity(VelocityTracker vt) { return mSwipeDirection == X ? vt.getXVelocity() : vt.getYVelocity(); } private ObjectAnimator createTranslationAnimation(View v, float newPos) { ObjectAnimator anim = ObjectAnimator.ofFloat(v, mSwipeDirection == X ? View.TRANSLATION_X : View.TRANSLATION_Y, newPos); return anim; } private float getPerpendicularVelocity(VelocityTracker vt) { return mSwipeDirection == X ? vt.getYVelocity() : vt.getXVelocity(); } private void setTranslation(View v, float translate) { if (mSwipeDirection == X) { v.setTranslationX(translate); } else { v.setTranslationY(translate); } } private float getSize(View v) { final DisplayMetrics dm = v.getContext().getResources().getDisplayMetrics(); return mSwipeDirection == X ? dm.widthPixels : dm.heightPixels; } public void setMinAlpha(float minAlpha) { mMinAlpha = minAlpha; } float getAlphaForOffset(View view) { float viewSize = getSize(view); final float fadeSize = ALPHA_FADE_END * viewSize; float result = 1.0f; float pos = getTranslation(view); if (pos >= viewSize * ALPHA_FADE_START) { result = 1.0f - (pos - viewSize * ALPHA_FADE_START) / fadeSize; } else if (pos < viewSize * (1.0f - ALPHA_FADE_START)) { result = 1.0f + (viewSize * ALPHA_FADE_START + pos) / fadeSize; } result = Math.min(result, 1.0f); result = Math.max(result, 0f); return Math.max(mMinAlpha, result); } /** * Determines whether the given view has RTL layout. */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) public static boolean isLayoutRtl(View view) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { return View.LAYOUT_DIRECTION_RTL == view.getLayoutDirection(); } else { return false; } } public boolean onInterceptTouchEvent(MotionEvent ev) { final int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: mDragging = false; mCurrView = mCallback.getChildAtPosition(ev); mVelocityTracker.clear(); if (mCurrView != null) { mRtl = isLayoutRtl(mCurrView); mCanCurrViewBeDimissed = mCallback.canChildBeDismissed(mCurrView); mVelocityTracker.addMovement(ev); mInitialTouchPos = getPos(ev); } else { mCanCurrViewBeDimissed = false; } break; case MotionEvent.ACTION_MOVE: if (mCurrView != null) { mVelocityTracker.addMovement(ev); float pos = getPos(ev); float delta = pos - mInitialTouchPos; if (Math.abs(delta) > mPagingTouchSlop) { mCallback.onBeginDrag(mCurrView); mDragging = true; mInitialTouchPos = pos - getTranslation(mCurrView); } } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mDragging = false; mCurrView = null; break; } return mDragging; } /** * @param view The view to be dismissed * @param velocity The desired pixels/second speed at which the view should move */ private void dismissChild(final View view, float velocity) { final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); float newPos; if (velocity < 0 || (velocity == 0 && getTranslation(view) < 0) // if we use the Menu to dismiss an item in landscape, animate up || (velocity == 0 && getTranslation(view) == 0 && mSwipeDirection == Y)) { newPos = -getSize(view); } else { newPos = getSize(view); } int duration = MAX_ESCAPE_ANIMATION_DURATION; if (velocity != 0) { duration = Math.min(duration, (int) (Math.abs(newPos - getTranslation(view)) * 1000f / Math.abs(velocity))); } else { duration = DEFAULT_ESCAPE_ANIMATION_DURATION; } ValueAnimator anim = createTranslationAnimation(view, newPos); anim.setInterpolator(sLinearInterpolator); anim.setDuration(duration); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { mCallback.onChildDismissed(view); if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { view.setAlpha(1.f); } } }); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { view.setAlpha(getAlphaForOffset(view)); } } }); anim.start(); } private void snapChild(final View view, float velocity) { final boolean canAnimViewBeDismissed = mCallback.canChildBeDismissed(view); ValueAnimator anim = createTranslationAnimation(view, 0); int duration = SNAP_ANIM_LEN; anim.setDuration(duration); anim.setInterpolator(DeckViewConfig.getInstance().linearOutSlowInInterpolator); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { view.setAlpha(getAlphaForOffset(view)); } mCallback.onSwipeChanged(mCurrView, view.getTranslationX()); } }); anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (FADE_OUT_DURING_SWIPE && canAnimViewBeDismissed) { view.setAlpha(1.0f); } mCallback.onSnapBackCompleted(view); } }); anim.start(); } public boolean onTouchEvent(MotionEvent ev) { if (!mDragging) { if (!onInterceptTouchEvent(ev)) { return mCanCurrViewBeDimissed; } } mVelocityTracker.addMovement(ev); final int action = ev.getAction(); switch (action) { case MotionEvent.ACTION_OUTSIDE: case MotionEvent.ACTION_MOVE: if (mCurrView != null) { float delta = getPos(ev) - mInitialTouchPos; setSwipeAmount(delta); mCallback.onSwipeChanged(mCurrView, delta); } break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if (mCurrView != null) { endSwipe(mVelocityTracker); } break; } return true; } private void setSwipeAmount(float amount) { // don't let items that can't be dismissed be dragged more than // maxScrollDistance if (CONSTRAIN_SWIPE && (!isValidSwipeDirection(amount) || !mCallback.canChildBeDismissed(mCurrView))) { float size = getSize(mCurrView); float maxScrollDistance = 0.15f * size; if (Math.abs(amount) >= size) { amount = amount > 0 ? maxScrollDistance : -maxScrollDistance; } else { amount = maxScrollDistance * (float) Math.sin((amount / size) * (Math.PI / 2)); } } setTranslation(mCurrView, amount); if (FADE_OUT_DURING_SWIPE && mCanCurrViewBeDimissed) { float alpha = getAlphaForOffset(mCurrView); mCurrView.setAlpha(alpha); } } private boolean isValidSwipeDirection(float amount) { if (mSwipeDirection == X) { if (mRtl) { return (amount <= 0) ? mAllowSwipeTowardsEnd : mAllowSwipeTowardsStart; } else { return (amount <= 0) ? mAllowSwipeTowardsStart : mAllowSwipeTowardsEnd; } } // Vertical swipes are always valid. return true; } private void endSwipe(VelocityTracker velocityTracker) { velocityTracker.computeCurrentVelocity(1000 /* px/sec */); float velocity = getVelocity(velocityTracker); float perpendicularVelocity = getPerpendicularVelocity(velocityTracker); float escapeVelocity = SWIPE_ESCAPE_VELOCITY * mDensityScale; float translation = getTranslation(mCurrView); // Decide whether to dismiss the current view boolean childSwipedFarEnough = DISMISS_IF_SWIPED_FAR_ENOUGH && Math.abs(translation) > 0.6 * getSize(mCurrView); boolean childSwipedFastEnough = (Math.abs(velocity) > escapeVelocity) && (Math.abs(velocity) > Math.abs(perpendicularVelocity)) && (velocity > 0) == (translation > 0); boolean dismissChild = mCallback.canChildBeDismissed(mCurrView) && isValidSwipeDirection(translation) && (childSwipedFastEnough || childSwipedFarEnough); if (dismissChild) { // flingadingy dismissChild(mCurrView, childSwipedFastEnough ? velocity : 0f); } else { // snappity mCallback.onDragCancelled(mCurrView); snapChild(mCurrView, velocity); } } public interface Callback { View getChildAtPosition(MotionEvent ev); boolean canChildBeDismissed(View v); void onBeginDrag(View v); void onSwipeChanged(View v, float delta); void onChildDismissed(View v); void onSnapBackCompleted(View v); void onDragCancelled(View v); } } ================================================ FILE: deckview/src/main/java/com/appeaser/deckview/helpers/FakeShadowDrawable.java ================================================ package com.appeaser.deckview.helpers; /** * Created by Vikram on 01/04/2015. */ import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.ColorFilter; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PixelFormat; import android.graphics.RadialGradient; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Shader; import android.graphics.drawable.Drawable; import android.util.Log; import com.appeaser.deckview.R; /** * A rounded rectangle drawable which also includes a shadow around. This is mostly copied from * frameworks/support/v7/cardview/eclair-mr1/android/support/v7/widget/ * RoundRectDrawableWithShadow.java revision c42ba8c000d1e6ce85e152dfc17089a0a69e739f with a few * modifications to suit our needs in SystemUI. */ public class FakeShadowDrawable extends Drawable { // used to calculate content padding final static double COS_45 = Math.cos(Math.toRadians(45)); final static float SHADOW_MULTIPLIER = 1.5f; final float mInsetShadow; // extra shadow to avoid gaps between card and shadow Paint mCornerShadowPaint; Paint mEdgeShadowPaint; final RectF mCardBounds; float mCornerRadius; Path mCornerShadowPath; // updated value with inset float mMaxShadowSize; // actual value set by developer float mRawMaxShadowSize; // multiplied value to account for shadow offset float mShadowSize; // actual value set by developer float mRawShadowSize; private boolean mDirty = true; private final int mShadowStartColor; private final int mShadowEndColor; private boolean mAddPaddingForCorners = true; /** * If shadow size is set to a value above max shadow, we print a warning */ private boolean mPrintedShadowClipWarning = false; public FakeShadowDrawable(Resources resources, DeckViewConfig config) { mShadowStartColor = resources.getColor(R.color.fake_shadow_start_color); mShadowEndColor = resources.getColor(R.color.fake_shadow_end_color); mInsetShadow = resources.getDimension(R.dimen.fake_shadow_inset); setShadowSize(resources.getDimensionPixelSize(R.dimen.fake_shadow_size), resources.getDimensionPixelSize(R.dimen.fake_shadow_size)); mCornerShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG); mCornerShadowPaint.setStyle(Paint.Style.FILL); mCornerShadowPaint.setDither(true); mCornerRadius = config.taskViewRoundedCornerRadiusPx; mCardBounds = new RectF(); mEdgeShadowPaint = new Paint(mCornerShadowPaint); } @Override public void setAlpha(int alpha) { mCornerShadowPaint.setAlpha(alpha); mEdgeShadowPaint.setAlpha(alpha); } @Override protected void onBoundsChange(Rect bounds) { super.onBoundsChange(bounds); mDirty = true; } void setShadowSize(float shadowSize, float maxShadowSize) { if (shadowSize < 0 || maxShadowSize < 0) { throw new IllegalArgumentException("invalid shadow size"); } if (shadowSize > maxShadowSize) { shadowSize = maxShadowSize; if (!mPrintedShadowClipWarning) { Log.w("CardView", "Shadow size is being clipped by the max shadow size. See " + "{CardView#setMaxCardElevation}."); mPrintedShadowClipWarning = true; } } if (mRawShadowSize == shadowSize && mRawMaxShadowSize == maxShadowSize) { return; } mRawShadowSize = shadowSize; mRawMaxShadowSize = maxShadowSize; mShadowSize = shadowSize * SHADOW_MULTIPLIER + mInsetShadow; mMaxShadowSize = maxShadowSize + mInsetShadow; mDirty = true; invalidateSelf(); } @Override public boolean getPadding(Rect padding) { int vOffset = (int) Math.ceil(calculateVerticalPadding(mRawMaxShadowSize, mCornerRadius, mAddPaddingForCorners)); int hOffset = (int) Math.ceil(calculateHorizontalPadding(mRawMaxShadowSize, mCornerRadius, mAddPaddingForCorners)); padding.set(hOffset, vOffset, hOffset, vOffset); return true; } static float calculateVerticalPadding(float maxShadowSize, float cornerRadius, boolean addPaddingForCorners) { if (addPaddingForCorners) { return (float) (maxShadowSize * SHADOW_MULTIPLIER + (1 - COS_45) * cornerRadius); } else { return maxShadowSize * SHADOW_MULTIPLIER; } } static float calculateHorizontalPadding(float maxShadowSize, float cornerRadius, boolean addPaddingForCorners) { if (addPaddingForCorners) { return (float) (maxShadowSize + (1 - COS_45) * cornerRadius); } else { return maxShadowSize; } } @Override public void setColorFilter(ColorFilter cf) { mCornerShadowPaint.setColorFilter(cf); mEdgeShadowPaint.setColorFilter(cf); } @Override public int getOpacity() { return PixelFormat.OPAQUE; } @Override public void draw(Canvas canvas) { if (mDirty) { buildComponents(getBounds()); mDirty = false; } canvas.translate(0, mRawShadowSize / 4); drawShadow(canvas); canvas.translate(0, -mRawShadowSize / 4); } private void drawShadow(Canvas canvas) { final float edgeShadowTop = -mCornerRadius - mShadowSize; final float inset = mCornerRadius + mInsetShadow + mRawShadowSize / 2; final boolean drawHorizontalEdges = mCardBounds.width() - 2 * inset > 0; final boolean drawVerticalEdges = mCardBounds.height() - 2 * inset > 0; // LT int saved = canvas.save(); canvas.translate(mCardBounds.left + inset, mCardBounds.top + inset); canvas.drawPath(mCornerShadowPath, mCornerShadowPaint); if (drawHorizontalEdges) { canvas.drawRect(0, edgeShadowTop, mCardBounds.width() - 2 * inset, -mCornerRadius, mEdgeShadowPaint); } canvas.restoreToCount(saved); // RB saved = canvas.save(); canvas.translate(mCardBounds.right - inset, mCardBounds.bottom - inset); canvas.rotate(180f); canvas.drawPath(mCornerShadowPath, mCornerShadowPaint); if (drawHorizontalEdges) { canvas.drawRect(0, edgeShadowTop, mCardBounds.width() - 2 * inset, -mCornerRadius + mShadowSize, mEdgeShadowPaint); } canvas.restoreToCount(saved); // LB saved = canvas.save(); canvas.translate(mCardBounds.left + inset, mCardBounds.bottom - inset); canvas.rotate(270f); canvas.drawPath(mCornerShadowPath, mCornerShadowPaint); if (drawVerticalEdges) { canvas.drawRect(0, edgeShadowTop, mCardBounds.height() - 2 * inset, -mCornerRadius, mEdgeShadowPaint); } canvas.restoreToCount(saved); // RT saved = canvas.save(); canvas.translate(mCardBounds.right - inset, mCardBounds.top + inset); canvas.rotate(90f); canvas.drawPath(mCornerShadowPath, mCornerShadowPaint); if (drawVerticalEdges) { canvas.drawRect(0, edgeShadowTop, mCardBounds.height() - 2 * inset, -mCornerRadius, mEdgeShadowPaint); } canvas.restoreToCount(saved); } private void buildShadowCorners() { RectF innerBounds = new RectF(-mCornerRadius, -mCornerRadius, mCornerRadius, mCornerRadius); RectF outerBounds = new RectF(innerBounds); outerBounds.inset(-mShadowSize, -mShadowSize); if (mCornerShadowPath == null) { mCornerShadowPath = new Path(); } else { mCornerShadowPath.reset(); } mCornerShadowPath.setFillType(Path.FillType.EVEN_ODD); mCornerShadowPath.moveTo(-mCornerRadius, 0); mCornerShadowPath.rLineTo(-mShadowSize, 0); // outer arc mCornerShadowPath.arcTo(outerBounds, 180f, 90f, false); // inner arc mCornerShadowPath.arcTo(innerBounds, 270f, -90f, false); mCornerShadowPath.close(); float startRatio = mCornerRadius / (mCornerRadius + mShadowSize); mCornerShadowPaint.setShader(new RadialGradient(0, 0, mCornerRadius + mShadowSize, new int[]{mShadowStartColor, mShadowStartColor, mShadowEndColor}, new float[]{0f, startRatio, 1f} , Shader.TileMode.CLAMP)); // we offset the content shadowSize/2 pixels up to make it more realistic. // this is why edge shadow shader has some extra space // When drawing bottom edge shadow, we use that extra space. mEdgeShadowPaint.setShader(new LinearGradient(0, -mCornerRadius + mShadowSize, 0, -mCornerRadius - mShadowSize, new int[]{mShadowStartColor, mShadowStartColor, mShadowEndColor}, new float[]{0f, .5f, 1f}, Shader.TileMode.CLAMP)); } private void buildComponents(Rect bounds) { // Card is offset SHADOW_MULTIPLIER * maxShadowSize to account for the shadow shift. // We could have different top-bottom offsets to avoid extra gap above but in that case // center aligning Views inside the CardView would be problematic. final float verticalOffset = mMaxShadowSize * SHADOW_MULTIPLIER; mCardBounds.set(bounds.left + mMaxShadowSize, bounds.top + verticalOffset, bounds.right - mMaxShadowSize, bounds.bottom - verticalOffset); buildShadowCorners(); } float getMinWidth() { final float content = 2 * Math.max(mRawMaxShadowSize, mCornerRadius + mInsetShadow + mRawMaxShadowSize / 2); return content + (mRawMaxShadowSize + mInsetShadow) * 2; } float getMinHeight() { final float content = 2 * Math.max(mRawMaxShadowSize, mCornerRadius + mInsetShadow + mRawMaxShadowSize * SHADOW_MULTIPLIER / 2); return content + (mRawMaxShadowSize * SHADOW_MULTIPLIER + mInsetShadow) * 2; } } ================================================ FILE: deckview/src/main/java/com/appeaser/deckview/utilities/DVConstants.java ================================================ package com.appeaser.deckview.utilities; /** * Created by Vikram on 02/04/2015. */ public class DVConstants { public static class DebugFlags { // Enable this with any other debug flag to see more info public static final boolean Verbose = false; public static class App { // Enables debug drawing for the transition thumbnail public static final boolean EnableTransitionThumbnailDebugMode = false; // Enables the filtering of tasks according to their grouping public static final boolean EnableTaskFiltering = false; // Enables clipping of tasks against each other public static final boolean EnableTaskStackClipping = true; // Enables tapping on the TaskBar to launch the task public static final boolean EnableTaskBarTouchEvents = true; // Enables app-info pane on long-pressing the icon public static final boolean EnableDevAppInfoOnLongPress = true; // Enables debug mode public static final boolean EnableDebugMode = false; // Enables the search bar layout public static final boolean EnableSearchLayout = true; // Enables the thumbnail alpha on the front-most task public static final boolean EnableThumbnailAlphaOnFrontmost = false; // This disables the bitmap and icon caches public static final boolean DisableBackgroundCache = false; // Enables the simulated task affiliations public static final boolean EnableSimulatedTaskGroups = false; // Defines the number of mock task affiliations per group public static final int TaskAffiliationsGroupCount = 12; // Enables us to create mock recents tasks public static final boolean EnableSystemServicesProxy = false; // Defines the number of mock recents packages to create public static final int SystemServicesProxyMockPackageCount = 3; // Defines the number of mock recents tasks to create public static final int SystemServicesProxyMockTaskCount = 100; } } public static class Values { public static class App { public static int AppWidgetHostId = 1024; public static String Key_SearchAppWidgetId = "searchAppWidgetId"; public static String Key_DebugModeEnabled = "debugModeEnabled"; public static String DebugModeVersion = "A"; } public static class DView { public static final int TaskStackMinOverscrollRange = 32; public static final int TaskStackMaxOverscrollRange = 128; public static final int FilterStartDelay = 25; } } } ================================================ FILE: deckview/src/main/java/com/appeaser/deckview/utilities/DVUtils.java ================================================ package com.appeaser.deckview.utilities; import android.animation.Animator; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.RectF; import android.view.View; import com.appeaser.deckview.helpers.DeckViewConfig; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; /** * Created by Vikram on 02/04/2015. */ public class DVUtils { // Reflection methods for altering shadows private static Method sPropertyMethod; static { try { Class c = Class.forName("android.view.GLES20Canvas"); sPropertyMethod = c.getDeclaredMethod("setProperty", String.class, String.class); if (!sPropertyMethod.isAccessible()) sPropertyMethod.setAccessible(true); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } } /** * Calculates a consistent animation duration (ms) for all animations depending on the movement * of the object being animated. */ public static int calculateTranslationAnimationDuration(int distancePx) { return calculateTranslationAnimationDuration(distancePx, 100); } public static int calculateTranslationAnimationDuration(int distancePx, int minDuration) { DeckViewConfig config = DeckViewConfig.getInstance(); return Math.max(minDuration, (int) (1000f /* ms/s */ * (Math.abs(distancePx) / config.animationPxMovementPerSecond))); } /** * Scales a rect about its centroid */ public static void scaleRectAboutCenter(Rect r, float scale) { if (scale != 1.0f) { int cx = r.centerX(); int cy = r.centerY(); r.offset(-cx, -cy); r.left = (int) (r.left * scale + 0.5f); r.top = (int) (r.top * scale + 0.5f); r.right = (int) (r.right * scale + 0.5f); r.bottom = (int) (r.bottom * scale + 0.5f); r.offset(cx, cy); } } /** * Maps a coorindate in a descendant view into the parent. */ public static float mapCoordInDescendentToSelf(View descendant, View root, float[] coord, boolean includeRootScroll) { ArrayList ancestorChain = new ArrayList(); float[] pt = {coord[0], coord[1]}; View v = descendant; while (v != root && v != null) { ancestorChain.add(v); v = (View) v.getParent(); } ancestorChain.add(root); float scale = 1.0f; int count = ancestorChain.size(); for (int i = 0; i < count; i++) { View v0 = ancestorChain.get(i); // For TextViews, scroll has a meaning which relates to the text position // which is very strange... ignore the scroll. if (v0 != descendant || includeRootScroll) { pt[0] -= v0.getScrollX(); pt[1] -= v0.getScrollY(); } v0.getMatrix().mapPoints(pt); pt[0] += v0.getLeft(); pt[1] += v0.getTop(); scale *= v0.getScaleX(); } coord[0] = pt[0]; coord[1] = pt[1]; return scale; } /** * Maps a coordinate in the root to a descendent. */ public static float mapCoordInSelfToDescendent(View descendant, View root, float[] coord, Matrix tmpInverseMatrix) { ArrayList ancestorChain = new ArrayList(); float[] pt = {coord[0], coord[1]}; View v = descendant; while (v != root) { ancestorChain.add(v); v = (View) v.getParent(); } ancestorChain.add(root); float scale = 1.0f; int count = ancestorChain.size(); tmpInverseMatrix.set(IDENTITY_MATRIX); for (int i = count - 1; i >= 0; i--) { View ancestor = ancestorChain.get(i); View next = i > 0 ? ancestorChain.get(i - 1) : null; pt[0] += ancestor.getScrollX(); pt[1] += ancestor.getScrollY(); if (next != null) { pt[0] -= next.getLeft(); pt[1] -= next.getTop(); next.getMatrix().invert(tmpInverseMatrix); tmpInverseMatrix.mapPoints(pt); scale *= next.getScaleX(); } } coord[0] = pt[0]; coord[1] = pt[1]; return scale; } /** * Calculates the constrast between two colors, using the algorithm provided by the WCAG v2. */ public static float computeContrastBetweenColors(int bg, int fg) { float bgR = Color.red(bg) / 255f; float bgG = Color.green(bg) / 255f; float bgB = Color.blue(bg) / 255f; bgR = (bgR < 0.03928f) ? bgR / 12.92f : (float) Math.pow((bgR + 0.055f) / 1.055f, 2.4f); bgG = (bgG < 0.03928f) ? bgG / 12.92f : (float) Math.pow((bgG + 0.055f) / 1.055f, 2.4f); bgB = (bgB < 0.03928f) ? bgB / 12.92f : (float) Math.pow((bgB + 0.055f) / 1.055f, 2.4f); float bgL = 0.2126f * bgR + 0.7152f * bgG + 0.0722f * bgB; float fgR = Color.red(fg) / 255f; float fgG = Color.green(fg) / 255f; float fgB = Color.blue(fg) / 255f; fgR = (fgR < 0.03928f) ? fgR / 12.92f : (float) Math.pow((fgR + 0.055f) / 1.055f, 2.4f); fgG = (fgG < 0.03928f) ? fgG / 12.92f : (float) Math.pow((fgG + 0.055f) / 1.055f, 2.4f); fgB = (fgB < 0.03928f) ? fgB / 12.92f : (float) Math.pow((fgB + 0.055f) / 1.055f, 2.4f); float fgL = 0.2126f * fgR + 0.7152f * fgG + 0.0722f * fgB; return Math.abs((fgL + 0.05f) / (bgL + 0.05f)); } /** * Returns the base color overlaid with another overlay color with a specified alpha. */ public static int getColorWithOverlay(int baseColor, int overlayColor, float overlayAlpha) { return Color.rgb( (int) (overlayAlpha * Color.red(baseColor) + (1f - overlayAlpha) * Color.red(overlayColor)), (int) (overlayAlpha * Color.green(baseColor) + (1f - overlayAlpha) * Color.green(overlayColor)), (int) (overlayAlpha * Color.blue(baseColor) + (1f - overlayAlpha) * Color.blue(overlayColor))); } /** * Sets some private shadow properties. */ public static void setShadowProperty(String property, String value) throws IllegalAccessException, InvocationTargetException { sPropertyMethod.invoke(null, property, value); } /** * Cancels an animation ensuring that if it has listeners, onCancel and onEnd * are not called. */ public static void cancelAnimationWithoutCallbacks(Animator animator) { if (animator != null) { animator.removeAllListeners(); animator.cancel(); } } public static Matrix IDENTITY_MATRIX = new Matrix() { void oops() { throw new IllegalStateException("Matrix can not be modified"); } @Override public void set(Matrix src) { oops(); } @Override public void reset() { oops(); } @Override public void setTranslate(float dx, float dy) { oops(); } @Override public void setScale(float sx, float sy, float px, float py) { oops(); } @Override public void setScale(float sx, float sy) { oops(); } @Override public void setRotate(float degrees, float px, float py) { oops(); } @Override public void setRotate(float degrees) { oops(); } @Override public void setSinCos(float sinValue, float cosValue, float px, float py) { oops(); } @Override public void setSinCos(float sinValue, float cosValue) { oops(); } @Override public void setSkew(float kx, float ky, float px, float py) { oops(); } @Override public void setSkew(float kx, float ky) { oops(); } @Override public boolean setConcat(Matrix a, Matrix b) { oops(); return false; } @Override public boolean preTranslate(float dx, float dy) { oops(); return false; } @Override public boolean preScale(float sx, float sy, float px, float py) { oops(); return false; } @Override public boolean preScale(float sx, float sy) { oops(); return false; } @Override public boolean preRotate(float degrees, float px, float py) { oops(); return false; } @Override public boolean preRotate(float degrees) { oops(); return false; } @Override public boolean preSkew(float kx, float ky, float px, float py) { oops(); return false; } @Override public boolean preSkew(float kx, float ky) { oops(); return false; } @Override public boolean preConcat(Matrix other) { oops(); return false; } @Override public boolean postTranslate(float dx, float dy) { oops(); return false; } @Override public boolean postScale(float sx, float sy, float px, float py) { oops(); return false; } @Override public boolean postScale(float sx, float sy) { oops(); return false; } @Override public boolean postRotate(float degrees, float px, float py) { oops(); return false; } @Override public boolean postRotate(float degrees) { oops(); return false; } @Override public boolean postSkew(float kx, float ky, float px, float py) { oops(); return false; } @Override public boolean postSkew(float kx, float ky) { oops(); return false; } @Override public boolean postConcat(Matrix other) { oops(); return false; } @Override public boolean setRectToRect(RectF src, RectF dst, ScaleToFit stf) { oops(); return false; } @Override public boolean setPolyToPoly(float[] src, int srcIndex, float[] dst, int dstIndex, int pointCount) { oops(); return false; } @Override public void setValues(float[] values) { oops(); } }; } ================================================ FILE: deckview/src/main/java/com/appeaser/deckview/utilities/DozeTrigger.java ================================================ package com.appeaser.deckview.utilities; /** * Created by Vikram on 01/04/2015. */ import android.os.Handler; /** * A dozer is a class that fires a trigger after it falls asleep. You can occasionally poke it to * wake it up, but it will fall asleep if left untouched. */ public class DozeTrigger { Handler mHandler; boolean mIsDozing; boolean mHasTriggered; int mDozeDurationSeconds; Runnable mSleepRunnable; // Sleep-runnable Runnable mDozeRunnable = new Runnable() { @Override public void run() { mSleepRunnable.run(); mIsDozing = false; mHasTriggered = true; } }; public DozeTrigger(int dozeDurationSeconds, Runnable sleepRunnable) { mHandler = new Handler(); mDozeDurationSeconds = dozeDurationSeconds; mSleepRunnable = sleepRunnable; } /** * Starts dozing. This also resets the trigger flag. */ public void startDozing() { forcePoke(); mHasTriggered = false; } /** * Stops dozing. */ public void stopDozing() { mHandler.removeCallbacks(mDozeRunnable); mIsDozing = false; } /** * Poke this dozer to wake it up for a little bit, if it is dozing. */ public void poke() { if (mIsDozing) { forcePoke(); } } /** * Poke this dozer to wake it up for a little bit. */ void forcePoke() { mHandler.removeCallbacks(mDozeRunnable); mHandler.postDelayed(mDozeRunnable, mDozeDurationSeconds * 1000); mIsDozing = true; } /** * Returns whether we are dozing or not. */ public boolean isDozing() { return mIsDozing; } /** * Returns whether the trigger has fired at least once. */ public boolean hasTriggered() { return mHasTriggered; } /** * Resets the doze trigger state. */ public void resetTrigger() { mHasTriggered = false; } } ================================================ FILE: deckview/src/main/java/com/appeaser/deckview/utilities/ReferenceCountedTrigger.java ================================================ package com.appeaser.deckview.utilities; /** * Created by Vikram on 01/04/2015. */ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.content.Context; import java.util.ArrayList; /** * A ref counted trigger that does some logic when the count is first incremented, or last * decremented. Not thread safe as it's not currently needed. */ public class ReferenceCountedTrigger { Context mContext; int mCount; ArrayList mFirstIncRunnables = new ArrayList(); ArrayList mLastDecRunnables = new ArrayList(); Runnable mErrorRunnable; // Convenience runnables Runnable mIncrementRunnable = new Runnable() { @Override public void run() { increment(); } }; Runnable mDecrementRunnable = new Runnable() { @Override public void run() { decrement(); } }; public ReferenceCountedTrigger(Context context, Runnable firstIncRunnable, Runnable lastDecRunnable, Runnable errorRunanable) { mContext = context; if (firstIncRunnable != null) mFirstIncRunnables.add(firstIncRunnable); if (lastDecRunnable != null) mLastDecRunnables.add(lastDecRunnable); mErrorRunnable = errorRunanable; } /** * Increments the ref count */ public void increment() { if (mCount == 0 && !mFirstIncRunnables.isEmpty()) { int numRunnables = mFirstIncRunnables.size(); for (int i = 0; i < numRunnables; i++) { mFirstIncRunnables.get(i).run(); } } mCount++; } /** * Convenience method to increment this trigger as a runnable */ public Runnable incrementAsRunnable() { return mIncrementRunnable; } /** * Adds a runnable to the last-decrement runnables list. */ public void addLastDecrementRunnable(Runnable r) { // To ensure that the last decrement always calls, we increment and decrement after setting // the last decrement runnable boolean ensureLastDecrement = (mCount == 0); if (ensureLastDecrement) increment(); mLastDecRunnables.add(r); if (ensureLastDecrement) decrement(); } /** * Decrements the ref count */ public void decrement() { mCount--; if (mCount == 0 && !mLastDecRunnables.isEmpty()) { int numRunnables = mLastDecRunnables.size(); for (int i = 0; i < numRunnables; i++) { mLastDecRunnables.get(i).run(); } } else if (mCount < 0) { if (mErrorRunnable != null) { mErrorRunnable.run(); } else { new Throwable("Invalid ref count").printStackTrace(); //Console.logError(mContext, "Invalid ref count"); } } } /** * Convenience method to decrement this trigger as a runnable. */ public Runnable decrementAsRunnable() { return mDecrementRunnable; } /** * Convenience method to decrement this trigger as a animator listener. */ public Animator.AnimatorListener decrementOnAnimationEnd() { return new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { decrement(); } }; } /** * Returns the current ref count */ public int getCount() { return mCount; } } ================================================ FILE: deckview/src/main/java/com/appeaser/deckview/views/AnimateableDeckChildViewBounds.java ================================================ package com.appeaser.deckview.views; import android.graphics.Outline; import android.graphics.Rect; import android.view.View; import android.view.ViewOutlineProvider; import com.appeaser.deckview.helpers.DeckViewConfig; /** * Created by Vikram on 02/04/2015. */ /* An outline provider that has a clip and outline that can be animated. */ public class AnimateableDeckChildViewBounds extends ViewOutlineProvider { DeckViewConfig mConfig; DeckChildView mSourceView; Rect mClipRect = new Rect(); Rect mClipBounds = new Rect(); int mCornerRadius; float mAlpha = 1f; final float mMinAlpha = 0.25f; public AnimateableDeckChildViewBounds(DeckChildView source, int cornerRadius) { mConfig = DeckViewConfig.getInstance(); mSourceView = source; mCornerRadius = cornerRadius; setClipBottom(getClipBottom()); } @Override public void getOutline(View view, Outline outline) { outline.setAlpha(mMinAlpha + mAlpha / (1f - mMinAlpha)); outline.setRoundRect(mClipRect.left, mClipRect.top, mSourceView.getWidth() - mClipRect.right, mSourceView.getHeight() - mClipRect.bottom, mCornerRadius); } /** * Sets the view outline alpha. */ void setAlpha(float alpha) { if (Float.compare(alpha, mAlpha) != 0) { mAlpha = alpha; mSourceView.invalidateOutline(); } } /** * Sets the bottom clip. */ public void setClipBottom(int bottom) { if (bottom != mClipRect.bottom) { mClipRect.bottom = bottom; mSourceView.invalidateOutline(); updateClipBounds(); if (!mConfig.useHardwareLayers) { mSourceView.mThumbnailView.updateThumbnailVisibility( bottom - mSourceView.getPaddingBottom()); } } } /** * Returns the bottom clip. */ public int getClipBottom() { return mClipRect.bottom; } private void updateClipBounds() { mClipBounds.set(mClipRect.left, mClipRect.top, mSourceView.getWidth() - mClipRect.right, mSourceView.getHeight() - mClipRect.bottom); mSourceView.setClipBounds(mClipBounds); } } ================================================ FILE: deckview/src/main/java/com/appeaser/deckview/views/DeckChildView.java ================================================ package com.appeaser.deckview.views; import android.animation.Animator; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.Rect; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.view.animation.AccelerateInterpolator; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; import android.widget.FrameLayout; import com.appeaser.deckview.R; import com.appeaser.deckview.helpers.DeckChildViewTransform; import com.appeaser.deckview.helpers.DeckViewConfig; import com.appeaser.deckview.helpers.FakeShadowDrawable; import com.appeaser.deckview.utilities.DVConstants; import com.appeaser.deckview.utilities.DVUtils; /** * Created by Vikram on 02/04/2015. */ /* A task view */ public class DeckChildView extends FrameLayout implements View.OnClickListener, View.OnLongClickListener { /** * The TaskView callbacks */ interface DeckChildViewCallbacks { public void onDeckChildViewAppIconClicked(DeckChildView dcv); public void onDeckChildViewAppInfoClicked(DeckChildView dcv); public void onDeckChildViewClicked(DeckChildView dcv, T key); public void onDeckChildViewDismissed(DeckChildView dcv); public void onDeckChildViewClipStateChanged(DeckChildView dcv); public void onDeckChildViewFocusChanged(DeckChildView dcv, boolean focused); } DeckViewConfig mConfig; float mTaskProgress; ObjectAnimator mTaskProgressAnimator; float mMaxDimScale; int mDimAlpha; AccelerateInterpolator mDimInterpolator = new AccelerateInterpolator(1f); PorterDuffColorFilter mDimColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_ATOP); Paint mDimLayerPaint = new Paint(); T mKey; boolean mTaskDataLoaded; boolean mIsFocused; boolean mFocusAnimationsEnabled; boolean mClipViewInStack; AnimateableDeckChildViewBounds mViewBounds; View mContent; DeckChildViewThumbnail mThumbnailView; DeckChildViewHeader mHeaderView; DeckChildViewCallbacks mCb; public static final Interpolator ALPHA_IN = new PathInterpolator(0.4f, 0f, 1f, 1f); // Optimizations ValueAnimator.AnimatorUpdateListener mUpdateDimListener = new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { setTaskProgress((Float) animation.getAnimatedValue()); } }; public DeckChildView(Context context) { this(context, null); } public DeckChildView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public DeckChildView(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public DeckChildView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); mConfig = DeckViewConfig.getInstance(); mMaxDimScale = mConfig.taskStackMaxDim / 255f; mClipViewInStack = true; mViewBounds = new AnimateableDeckChildViewBounds(this, mConfig.taskViewRoundedCornerRadiusPx); setTaskProgress(getTaskProgress()); setDim(getDim()); if (mConfig.fakeShadows) { setBackground(new FakeShadowDrawable(context.getResources(), mConfig)); } setOutlineProvider(mViewBounds); } /** * Set callback */ void setCallbacks(DeckChildViewCallbacks cb) { mCb = cb; } /** * Resets this TaskView for reuse. */ void reset() { resetViewProperties(); resetNoUserInteractionState(); setClipViewInStack(false); setCallbacks(null); } /** * Gets the task */ T getAttachedKey() { return mKey; } /** * Returns the view bounds. */ AnimateableDeckChildViewBounds getViewBounds() { return mViewBounds; } @Override protected void onFinishInflate() { // Bind the views mContent = findViewById(R.id.task_view_content); mHeaderView = (DeckChildViewHeader) findViewById(R.id.task_view_bar); mThumbnailView = (DeckChildViewThumbnail) findViewById(R.id.task_view_thumbnail); mThumbnailView.updateClipToTaskBar(mHeaderView); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); int widthWithoutPadding = width - getPaddingLeft() - getPaddingRight(); int heightWithoutPadding = height - getPaddingTop() - getPaddingBottom(); // Measure the content mContent.measure(MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY)); // Measure the bar view, and action button mHeaderView.measure(MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mConfig.taskBarHeight, MeasureSpec.EXACTLY)); // Measure the thumbnail to be square mThumbnailView.measure( MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(widthWithoutPadding, MeasureSpec.EXACTLY)); setMeasuredDimension(width, height); invalidateOutline(); } /** * Synchronizes this view's properties with the task's transform */ void updateViewPropertiesToTaskTransform(DeckChildViewTransform toTransform, int duration) { updateViewPropertiesToTaskTransform(toTransform, duration, null); } void updateViewPropertiesToTaskTransform(DeckChildViewTransform toTransform, int duration, ValueAnimator.AnimatorUpdateListener updateCallback) { // Apply the transform toTransform.applyToTaskView(this, duration, mConfig.fastOutSlowInInterpolator, false, !mConfig.fakeShadows, updateCallback); // Update the task progress DVUtils.cancelAnimationWithoutCallbacks(mTaskProgressAnimator); if (duration <= 0) { setTaskProgress(toTransform.p); } else { mTaskProgressAnimator = ObjectAnimator.ofFloat(this, "taskProgress", toTransform.p); mTaskProgressAnimator.setDuration(duration); mTaskProgressAnimator.addUpdateListener(mUpdateDimListener); mTaskProgressAnimator.start(); } } /** * Resets this view's properties */ void resetViewProperties() { setDim(0); setLayerType(View.LAYER_TYPE_NONE, null); DeckChildViewTransform.reset(this); } /** * When we are un/filtering, this method will set up the transform that we are animating to, * in order to hide the task. */ void prepareTaskTransformForFilterTaskHidden(DeckChildViewTransform toTransform) { // Fade the view out and slide it away toTransform.alpha = 0f; toTransform.translationY += 200; toTransform.translationZ = 0; } /** * When we are un/filtering, this method will setup the transform that we are animating from, * in order to show the task. */ void prepareTaskTransformForFilterTaskVisible(DeckChildViewTransform fromTransform) { // Fade the view in fromTransform.alpha = 0f; } /** * Prepares this task view for the enter-recents animations. This is called earlier in the * first layout because the actual animation into recents may take a long time. */ void prepareEnterRecentsAnimation(boolean isTaskViewLaunchTargetTask, boolean occludesLaunchTarget, int offscreenY) { int initialDim = getDim(); if (mConfig.launchedHasConfigurationChanged) { // Just load the views as-is } else if (mConfig.launchedFromAppWithThumbnail) { if (isTaskViewLaunchTargetTask) { // Set the dim to 0 so we can animate it in initialDim = 0; } else if (occludesLaunchTarget) { // Move the task view off screen (below) so we can animate it in setTranslationY(offscreenY); } } else if (mConfig.launchedFromHome) { // Move the task view off screen (below) so we can animate it in setTranslationY(offscreenY); setTranslationZ(0); setScaleX(1f); setScaleY(1f); } // Apply the current dim setDim(initialDim); // Prepare the thumbnail view alpha mThumbnailView.prepareEnterRecentsAnimation(isTaskViewLaunchTargetTask); } /** * Animates this task view as it enters recents */ void startEnterRecentsAnimation(final ViewAnimation.TaskViewEnterContext ctx) { Log.i(getClass().getSimpleName(), "startEnterRecentsAnimation"); final DeckChildViewTransform transform = ctx.currentTaskTransform; int startDelay = 0; if (mConfig.launchedFromHome) { Log.i(getClass().getSimpleName(), "mConfig.launchedFromHome false"); // Animate the tasks up int frontIndex = (ctx.currentStackViewCount - ctx.currentStackViewIndex - 1); int delay = mConfig.transitionEnterFromHomeDelay + frontIndex * mConfig.taskViewEnterFromHomeStaggerDelay; setScaleX(transform.scale); setScaleY(transform.scale); if (!mConfig.fakeShadows) { animate().translationZ(transform.translationZ); } animate() .translationY(transform.translationY) .setStartDelay(delay) .setUpdateListener(ctx.updateListener) .setInterpolator(mConfig.quintOutInterpolator) .setDuration(mConfig.taskViewEnterFromHomeDuration + frontIndex * mConfig.taskViewEnterFromHomeStaggerDelay) .withEndAction(new Runnable() { @Override public void run() { // Decrement the post animation trigger ctx.postAnimationTrigger.decrement(); } }) .start(); ctx.postAnimationTrigger.increment(); startDelay = delay; } // Enable the focus animations from this point onwards so that they aren't affected by the // window transitions postDelayed(new Runnable() { @Override public void run() { enableFocusAnimations(); } }, startDelay); } /** * Animates this task view as it leaves recents by pressing home. */ void startExitToHomeAnimation(ViewAnimation.TaskViewExitContext ctx) { animate() .translationY(ctx.offscreenTranslationY) .setStartDelay(0) .setUpdateListener(null) .setInterpolator(mConfig.fastOutLinearInInterpolator) .setDuration(mConfig.taskViewExitToHomeDuration) .withEndAction(ctx.postAnimationTrigger.decrementAsRunnable()) .start(); ctx.postAnimationTrigger.increment(); } /** * Animates this task view as it exits recents */ void startLaunchTaskAnimation(final Runnable postAnimRunnable, boolean isLaunchingTask, boolean occludesLaunchTarget, boolean lockToTask) { if (isLaunchingTask) { // Animate the thumbnail alpha back into full opacity for the window animation out mThumbnailView.startLaunchTaskAnimation(postAnimRunnable); // Animate the dim if (mDimAlpha > 0) { ObjectAnimator anim = ObjectAnimator.ofInt(this, "dim", 0); anim.setDuration(mConfig.taskViewExitToAppDuration); anim.setInterpolator(mConfig.fastOutLinearInInterpolator); anim.start(); } } else { // Hide the dismiss button mHeaderView.startLaunchTaskDismissAnimation(); // If this is another view in the task grouping and is in front of the launch task, // animate it away first if (occludesLaunchTarget) { animate().alpha(0f) .translationY(getTranslationY() + mConfig.taskViewAffiliateGroupEnterOffsetPx) .setStartDelay(0) .setUpdateListener(null) .setInterpolator(mConfig.fastOutLinearInInterpolator) .setDuration(mConfig.taskViewExitToAppDuration) .start(); } } } /** * Animates the deletion of this task view */ void startDeleteTaskAnimation(final Runnable r) { // Disabling clipping with the stack while the view is animating away setClipViewInStack(false); animate().translationX(mConfig.taskViewRemoveAnimTranslationXPx) .alpha(0f) .setStartDelay(0) .setUpdateListener(null) .setInterpolator(mConfig.fastOutSlowInInterpolator) .setDuration(mConfig.taskViewRemoveAnimDuration) .withEndAction(new Runnable() { @Override public void run() { // We just throw this into a runnable because starting a view property // animation using layers can cause inconsisten results if we try and // update the layers while the animation is running. In some cases, // the runnabled passed in may start an animation which also uses layers // so we defer all this by posting this. r.run(); // Re-enable clipping with the stack (we will reuse this view) setClipViewInStack(true); } }) .start(); } /** * Animates this task view if the user does not interact with the stack after a certain time. */ void startNoUserInteractionAnimation() { mHeaderView.startNoUserInteractionAnimation(); } /** * Mark this task view that the user does has not interacted with the stack after a certain time. */ void setNoUserInteractionState() { mHeaderView.setNoUserInteractionState(); } /** * Resets the state tracking that the user has not interacted with the stack after a certain time. */ void resetNoUserInteractionState() { mHeaderView.resetNoUserInteractionState(); } /** * Dismisses this task. */ void dismissTask() { // Animate out the view and call the callback final DeckChildView tv = this; startDeleteTaskAnimation(new Runnable() { @Override public void run() { if (mCb != null) { mCb.onDeckChildViewDismissed(tv); } } }); } /** * Returns whether this view should be clipped, or any views below should clip against this * view. */ boolean shouldClipViewInStack() { return mClipViewInStack && (getVisibility() == View.VISIBLE); } /** * Sets whether this view should be clipped, or clipped against. */ public void setClipViewInStack(boolean clip) { if (clip != mClipViewInStack) { mClipViewInStack = clip; if (mCb != null) { mCb.onDeckChildViewClipStateChanged(this); } } } /** * Sets the current task progress. */ public void setTaskProgress(float p) { mTaskProgress = p; mViewBounds.setAlpha(p); updateDimFromTaskProgress(); } /** * Returns the current task progress. */ public float getTaskProgress() { return mTaskProgress; } /** * Returns the current dim. */ public void setDim(int dim) { mDimAlpha = dim; if (mConfig.useHardwareLayers) { // Defer setting hardware layers if we have not yet measured, or there is no dim to draw if (getMeasuredWidth() > 0 && getMeasuredHeight() > 0) { mDimColorFilter = new PorterDuffColorFilter(Color.argb(mDimAlpha, 0, 0, 0), PorterDuff.Mode.SRC_ATOP); mDimLayerPaint.setColorFilter(mDimColorFilter); mContent.setLayerType(LAYER_TYPE_HARDWARE, mDimLayerPaint); } } else { float dimAlpha = mDimAlpha / 255.0f; if (mThumbnailView != null) { mThumbnailView.setDimAlpha(dimAlpha); } if (mHeaderView != null) { mHeaderView.setDimAlpha(dim); } } } /** * Returns the current dim. */ public int getDim() { return mDimAlpha; } /** * Animates the dim to the task progress. */ void animateDimToProgress(int delay, int duration, Animator.AnimatorListener postAnimRunnable) { // Animate the dim into view as well int toDim = getDimFromTaskProgress(); if (toDim != getDim()) { ObjectAnimator anim = ObjectAnimator.ofInt(DeckChildView.this, "dim", toDim); anim.setStartDelay(delay); anim.setDuration(duration); if (postAnimRunnable != null) { anim.addListener(postAnimRunnable); } anim.start(); } } /** * Compute the dim as a function of the scale of this view. */ int getDimFromTaskProgress() { float dim = mMaxDimScale * mDimInterpolator.getInterpolation(1f - mTaskProgress); return (int) (dim * 255); } /** * Update the dim as a function of the scale of this view. */ void updateDimFromTaskProgress() { setDim(getDimFromTaskProgress()); } /**** View focus state ****/ /** * Sets the focused task explicitly. We need a separate flag because requestFocus() won't happen * if the view is not currently visible, or we are in touch state (where we still want to keep * track of focus). */ public void setFocusedTask(boolean animateFocusedState) { mIsFocused = true; if (mFocusAnimationsEnabled) { // Focus the header bar mHeaderView.onTaskViewFocusChanged(true, animateFocusedState); } // Update the thumbnail alpha with the focus mThumbnailView.onFocusChanged(true); // Call the callback if (mCb != null) { mCb.onDeckChildViewFocusChanged(this, true); } // Workaround, we don't always want it focusable in touch mode, but we want the first task // to be focused after the enter-recents animation, which can be triggered from either touch // or keyboard setFocusableInTouchMode(true); requestFocus(); setFocusableInTouchMode(false); invalidate(); } /** * Unsets the focused task explicitly. */ void unsetFocusedTask() { mIsFocused = false; if (mFocusAnimationsEnabled) { // Un-focus the header bar mHeaderView.onTaskViewFocusChanged(false, true); } // Update the thumbnail alpha with the focus mThumbnailView.onFocusChanged(false); // Call the callback if (mCb != null) { mCb.onDeckChildViewFocusChanged(this, false); } invalidate(); } /** * Updates the explicitly focused state when the view focus changes. */ @Override protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) { super.onFocusChanged(gainFocus, direction, previouslyFocusedRect); if (!gainFocus) { unsetFocusedTask(); } } /** * Returns whether we have explicitly been focused. */ public boolean isFocusedTask() { return mIsFocused || isFocused(); } /** * Enables all focus animations. */ void enableFocusAnimations() { boolean wasFocusAnimationsEnabled = mFocusAnimationsEnabled; mFocusAnimationsEnabled = true; if (mIsFocused && !wasFocusAnimationsEnabled) { // Re-notify the header if we were focused and animations were not previously enabled mHeaderView.onTaskViewFocusChanged(true, true); } } /**** TaskCallbacks Implementation ****/ /** * Binds this task view to the task */ public void onTaskBound(T key) { mKey = key; } private boolean isBound() { return mKey != null; } /** * Binds this task view to the task */ public void onTaskUnbound() { mKey = null; } public Bitmap getThumbnail() { if (mThumbnailView != null) { return mThumbnailView.getThumbnail(); } return null; } public void onDataLoaded(T key, Bitmap thumbnail, Drawable headerIcon, String headerTitle, int headerBgColor) { if (!isBound() || !mKey.equals(key)) return; if (mThumbnailView != null && mHeaderView != null) { // Bind each of the views to the new task data mThumbnailView.rebindToTask(thumbnail); mHeaderView.rebindToTask(headerIcon, headerTitle, headerBgColor); // Rebind any listeners mHeaderView.mApplicationIcon.setOnClickListener(this); mHeaderView.mDismissButton.setOnClickListener(this); // TODO: Check if this functionality is needed mHeaderView.mApplicationIcon.setOnLongClickListener(this); } mTaskDataLoaded = true; } public void onDataUnloaded() { if (mThumbnailView != null && mHeaderView != null) { // Unbind each of the views from the task data and remove the task callback mThumbnailView.unbindFromTask(); mHeaderView.unbindFromTask(); // Unbind any listeners mHeaderView.mApplicationIcon.setOnClickListener(null); mHeaderView.mDismissButton.setOnClickListener(null); if (DVConstants.DebugFlags.App.EnableDevAppInfoOnLongPress) { mHeaderView.mApplicationIcon.setOnLongClickListener(null); } } mTaskDataLoaded = false; } /** * Enables/disables handling touch on this task view. */ public void setTouchEnabled(boolean enabled) { setOnClickListener(enabled ? this : null); } /** * * View.OnClickListener Implementation *** */ @Override public void onClick(final View v) { final DeckChildView tv = this; final boolean delayViewClick = (v != this); if (delayViewClick) { // We purposely post the handler delayed to allow for the touch feedback to draw postDelayed(new Runnable() { @Override public void run() { if (DVConstants.DebugFlags.App.EnableTaskFiltering && v == mHeaderView.mApplicationIcon) { if (mCb != null) { mCb.onDeckChildViewAppIconClicked(tv); } } else if (v == mHeaderView.mDismissButton) { dismissTask(); } } }, 125); } else { if (mCb != null) { mCb.onDeckChildViewClicked(tv, tv.getAttachedKey()); } } } /** * * View.OnLongClickListener Implementation *** */ @Override public boolean onLongClick(View v) { if (v == mHeaderView.mApplicationIcon) { if (mCb != null) { mCb.onDeckChildViewAppInfoClicked(this); return true; } } return false; } } ================================================ FILE: deckview/src/main/java/com/appeaser/deckview/views/DeckChildViewHeader.java ================================================ package com.appeaser.deckview.views; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ArgbEvaluator; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Outline; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.PorterDuffXfermode; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.RippleDrawable; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewOutlineProvider; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; import com.appeaser.deckview.R; import com.appeaser.deckview.helpers.DeckViewConfig; import com.appeaser.deckview.utilities.DVConstants; import com.appeaser.deckview.utilities.DVUtils; /** * Created by Vikram on 02/04/2015. */ /* The task bar view */ public class DeckChildViewHeader extends FrameLayout { DeckViewConfig mConfig; // Header views ImageView mDismissButton; ImageView mApplicationIcon; TextView mActivityDescription; // Header drawables boolean mCurrentPrimaryColorIsDark; int mCurrentPrimaryColor; int mBackgroundColor; Drawable mLightDismissDrawable; Drawable mDarkDismissDrawable; RippleDrawable mBackground; GradientDrawable mBackgroundColorDrawable; AnimatorSet mFocusAnimator; String mDismissContentDescription; // Static highlight that we draw at the top of each view static Paint sHighlightPaint; // Header dim, which is only used when task view hardware layers are not used Paint mDimLayerPaint = new Paint(); PorterDuffColorFilter mDimColorFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_ATOP); public DeckChildViewHeader(Context context) { this(context, null); } public DeckChildViewHeader(Context context, AttributeSet attrs) { this(context, attrs, 0); } public DeckChildViewHeader(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public DeckChildViewHeader(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); mConfig = DeckViewConfig.getInstance(); setWillNotDraw(false); setClipToOutline(true); setOutlineProvider(new ViewOutlineProvider() { @Override public void getOutline(View view, Outline outline) { outline.setRect(0, 0, getMeasuredWidth(), getMeasuredHeight()); } }); // Load the dismiss resources Resources res = context.getResources(); mLightDismissDrawable = res.getDrawable(R.drawable.deck_child_view_dismiss_light); mDarkDismissDrawable = res.getDrawable(R.drawable.deck_child_view_dismiss_dark); mDismissContentDescription = res.getString(R.string.accessibility_item_will_be_dismissed); // Configure the highlight paint if (sHighlightPaint == null) { sHighlightPaint = new Paint(); sHighlightPaint.setStyle(Paint.Style.STROKE); sHighlightPaint.setStrokeWidth(mConfig.taskViewHighlightPx); sHighlightPaint.setColor(mConfig.taskBarViewHighlightColor); sHighlightPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.ADD)); sHighlightPaint.setAntiAlias(true); } } @Override public boolean onTouchEvent(MotionEvent event) { // We ignore taps on the task bar except on the filter and dismiss buttons if (!DVConstants.DebugFlags.App.EnableTaskBarTouchEvents) return true; return super.onTouchEvent(event); } @Override protected void onFinishInflate() { // Initialize the icon and description views mApplicationIcon = (ImageView) findViewById(R.id.application_icon); mActivityDescription = (TextView) findViewById(R.id.activity_description); mDismissButton = (ImageView) findViewById(R.id.dismiss_task); // Hide the backgrounds if they are ripple drawables if (!DVConstants.DebugFlags.App.EnableTaskFiltering) { if (mApplicationIcon.getBackground() instanceof RippleDrawable) { mApplicationIcon.setBackground(null); } } mBackgroundColorDrawable = (GradientDrawable) getContext().getDrawable(R.drawable .deck_child_view_header_bg_color); // Copy the ripple drawable since we are going to be manipulating it mBackground = (RippleDrawable) getContext().getDrawable(R.drawable.deck_child_view_header_bg); mBackground = (RippleDrawable) mBackground.mutate().getConstantState().newDrawable(); mBackground.setColor(ColorStateList.valueOf(0)); mBackground.setDrawableByLayerId(mBackground.getId(0), mBackgroundColorDrawable); setBackground(mBackground); } @Override protected void onDraw(Canvas canvas) { // Draw the highlight at the top edge (but put the bottom edge just out of view) float offset = (float) Math.ceil(mConfig.taskViewHighlightPx / 2f); float radius = mConfig.taskViewRoundedCornerRadiusPx; int count = canvas.save(Canvas.CLIP_SAVE_FLAG); canvas.clipRect(0, 0, getMeasuredWidth(), getMeasuredHeight()); canvas.drawRoundRect(-offset, 0f, (float) getMeasuredWidth() + offset, getMeasuredHeight() + radius, radius, radius, sHighlightPaint); canvas.restoreToCount(count); } @Override public boolean hasOverlappingRendering() { return false; } /** * Sets the dim alpha, only used when we are not using hardware layers. * (see RecentsConfiguration.useHardwareLayers) */ void setDimAlpha(int alpha) { mDimColorFilter = new PorterDuffColorFilter(Color.argb(alpha, 0, 0, 0), PorterDuff.Mode.SRC_ATOP); mDimLayerPaint.setColorFilter(mDimColorFilter); setLayerType(LAYER_TYPE_HARDWARE, mDimLayerPaint); } /** * Returns the secondary color for a primary color. */ int getSecondaryColor(int primaryColor, boolean useLightOverlayColor) { int overlayColor = useLightOverlayColor ? Color.WHITE : Color.BLACK; return DVUtils.getColorWithOverlay(primaryColor, overlayColor, 0.8f); } /** * Binds the bar view to the task */ //public void rebindToTask(Task t) { public void rebindToTask(Drawable headerIcon, String headerTitle, int headerBgColor) { // If an activity icon is defined, then we use that as the primary icon to show in the bar, // otherwise, we fall back to the application icon mApplicationIcon.setImageDrawable(headerIcon); mApplicationIcon.setContentDescription(headerTitle); mActivityDescription.setText(headerTitle); // Try and apply the system ui tint int existingBgColor = (getBackground() instanceof ColorDrawable) ? ((ColorDrawable) getBackground()).getColor() : 0; if (existingBgColor != headerBgColor) { mBackgroundColorDrawable.setColor(headerBgColor); mBackgroundColor = headerBgColor; } mCurrentPrimaryColor = headerBgColor; //mCurrentPrimaryColorIsDark = t.useLightOnPrimaryColor; mActivityDescription.setTextColor(mConfig.taskBarViewLightTextColor); mDismissButton.setImageDrawable(mLightDismissDrawable); mDismissButton.setContentDescription(String.format(mDismissContentDescription, headerTitle)); } /** * Unbinds the bar view from the task */ void unbindFromTask() { mApplicationIcon.setImageDrawable(null); } /** * Animates this task bar dismiss button when launching a task. */ void startLaunchTaskDismissAnimation() { if (mDismissButton.getVisibility() == View.VISIBLE) { mDismissButton.animate().cancel(); mDismissButton.animate() .alpha(0f) .setStartDelay(0) .setInterpolator(mConfig.fastOutSlowInInterpolator) .setDuration(mConfig.taskViewExitToAppDuration) .withLayer() .start(); } } /** * Animates this task bar if the user does not interact with the stack after a certain time. */ void startNoUserInteractionAnimation() { if (mDismissButton.getVisibility() != View.VISIBLE) { mDismissButton.setVisibility(View.VISIBLE); mDismissButton.setAlpha(0f); mDismissButton.animate() .alpha(1f) .setStartDelay(0) .setInterpolator(mConfig.fastOutLinearInInterpolator) .setDuration(mConfig.taskViewEnterFromAppDuration) .withLayer() .start(); } } /** * Mark this task view that the user does has not interacted with the stack after a certain time. */ void setNoUserInteractionState() { if (mDismissButton.getVisibility() != View.VISIBLE) { mDismissButton.animate().cancel(); mDismissButton.setVisibility(View.VISIBLE); mDismissButton.setAlpha(1f); } } /** * Resets the state tracking that the user has not interacted with the stack after a certain time. */ void resetNoUserInteractionState() { mDismissButton.setVisibility(View.INVISIBLE); } @Override protected int[] onCreateDrawableState(int extraSpace) { // Don't forward our state to the drawable - we do it manually in onTaskViewFocusChanged. // This is to prevent layer trashing when the view is pressed. return new int[]{}; } /** * Notifies the associated TaskView has been focused. */ void onTaskViewFocusChanged(boolean focused, boolean animateFocusedState) { // If we are not animating the visible state, just return if (!animateFocusedState) return; boolean isRunning = false; if (mFocusAnimator != null) { isRunning = mFocusAnimator.isRunning(); DVUtils.cancelAnimationWithoutCallbacks(mFocusAnimator); } if (focused) { int secondaryColor = getSecondaryColor(mCurrentPrimaryColor, mCurrentPrimaryColorIsDark); int[][] states = new int[][]{ new int[]{android.R.attr.state_enabled}, new int[]{android.R.attr.state_pressed} }; int[] newStates = new int[]{ android.R.attr.state_enabled, android.R.attr.state_pressed }; int[] colors = new int[]{ secondaryColor, secondaryColor }; mBackground.setColor(new ColorStateList(states, colors)); mBackground.setState(newStates); // Pulse the background color int currentColor = mBackgroundColor; int lightPrimaryColor = getSecondaryColor(mCurrentPrimaryColor, mCurrentPrimaryColorIsDark); ValueAnimator backgroundColor = ValueAnimator.ofObject(new ArgbEvaluator(), currentColor, lightPrimaryColor); backgroundColor.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { mBackground.setState(new int[]{}); } }); backgroundColor.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { int color = (int) animation.getAnimatedValue(); mBackgroundColorDrawable.setColor(color); mBackgroundColor = color; } }); backgroundColor.setRepeatCount(ValueAnimator.INFINITE); backgroundColor.setRepeatMode(ValueAnimator.REVERSE); // Pulse the translation ObjectAnimator translation = ObjectAnimator.ofFloat(this, "translationZ", 15f); translation.setRepeatCount(ValueAnimator.INFINITE); translation.setRepeatMode(ValueAnimator.REVERSE); mFocusAnimator = new AnimatorSet(); mFocusAnimator.playTogether(backgroundColor, translation); mFocusAnimator.setStartDelay(750); mFocusAnimator.setDuration(750); mFocusAnimator.start(); } else { if (isRunning) { // Restore the background color int currentColor = mBackgroundColor; ValueAnimator backgroundColor = ValueAnimator.ofObject(new ArgbEvaluator(), currentColor, mCurrentPrimaryColor); backgroundColor.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { int color = (int) animation.getAnimatedValue(); mBackgroundColorDrawable.setColor(color); mBackgroundColor = color; } }); // Restore the translation ObjectAnimator translation = ObjectAnimator.ofFloat(this, "translationZ", 0f); mFocusAnimator = new AnimatorSet(); mFocusAnimator.playTogether(backgroundColor, translation); mFocusAnimator.setDuration(150); mFocusAnimator.start(); } else { mBackground.setState(new int[]{}); setTranslationZ(0f); } } } } ================================================ FILE: deckview/src/main/java/com/appeaser/deckview/views/DeckChildViewThumbnail.java ================================================ package com.appeaser.deckview.views; /** * Created by Vikram on 02/04/2015. */ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapShader; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.LightingColorFilter; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Shader; import android.util.AttributeSet; import android.view.View; import com.appeaser.deckview.helpers.DeckViewConfig; import com.appeaser.deckview.utilities.DVUtils; /** * The task thumbnail view. It implements an image view that allows for animating the dim and * alpha of the thumbnail image. */ public class DeckChildViewThumbnail extends View { DeckViewConfig mConfig; // Drawing float mDimAlpha; Matrix mScaleMatrix = new Matrix(); Paint mDrawPaint = new Paint(); RectF mBitmapRect = new RectF(); RectF mLayoutRect = new RectF(); BitmapShader mBitmapShader; LightingColorFilter mLightingColorFilter = new LightingColorFilter(0xffffffff, 0); // Thumbnail alpha float mThumbnailAlpha; ValueAnimator mThumbnailAlphaAnimator; ValueAnimator.AnimatorUpdateListener mThumbnailAlphaUpdateListener = new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mThumbnailAlpha = (float) animation.getAnimatedValue(); updateThumbnailPaintFilter(); } }; // Task bar clipping, the top of this thumbnail can be clipped against the opaque header // bar that overlaps this thumbnail View mTaskBar; Rect mClipRect = new Rect(); // Visibility optimization, if the thumbnail height is less than the height of the header // bar for the task view, then just mark this thumbnail view as invisible boolean mInvisible; public DeckChildViewThumbnail(Context context) { this(context, null); } public DeckChildViewThumbnail(Context context, AttributeSet attrs) { this(context, attrs, 0); } public DeckChildViewThumbnail(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public DeckChildViewThumbnail(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); mConfig = DeckViewConfig.getInstance(); mDrawPaint.setColorFilter(mLightingColorFilter); mDrawPaint.setFilterBitmap(true); mDrawPaint.setAntiAlias(true); } @Override protected void onFinishInflate() { mThumbnailAlpha = mConfig.taskViewThumbnailAlpha; updateThumbnailPaintFilter(); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (changed) { mLayoutRect.set(0, 0, getWidth(), getHeight()); updateThumbnailScale(); } } @Override protected void onDraw(Canvas canvas) { if (mInvisible) { return; } // Draw the thumbnail with the rounded corners canvas.drawRoundRect(0, 0, getWidth(), getHeight(), mConfig.taskViewRoundedCornerRadiusPx, mConfig.taskViewRoundedCornerRadiusPx, mDrawPaint); } /** * Sets the thumbnail to a given bitmap. */ void setThumbnail(Bitmap bm) { mThumbnail = bm; if (bm != null) { mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); mDrawPaint.setShader(mBitmapShader); mBitmapRect.set(0, 0, bm.getWidth(), bm.getHeight()); updateThumbnailScale(); } else { mBitmapShader = null; mDrawPaint.setShader(null); } updateThumbnailPaintFilter(); } /** * Updates the paint to draw the thumbnail. */ void updateThumbnailPaintFilter() { if (mInvisible) { return; } int mul = (int) ((1.0f - mDimAlpha) * mThumbnailAlpha * 255); int add = (int) ((1.0f - mDimAlpha) * (1 - mThumbnailAlpha) * 255); if (mBitmapShader != null) { mLightingColorFilter = new LightingColorFilter(Color.argb(255, mul, mul, mul), Color.argb(0, add, add, add)); mDrawPaint.setColorFilter(mLightingColorFilter); mDrawPaint.setColor(0xffffffff); } else { int grey = mul + add; mDrawPaint.setColorFilter(null); mDrawPaint.setColor(Color.argb(255, grey, grey, grey)); } invalidate(); } /** * Updates the thumbnail shader's scale transform. */ void updateThumbnailScale() { if (mBitmapShader != null) { mScaleMatrix.setRectToRect(mBitmapRect, mLayoutRect, Matrix.ScaleToFit.FILL); mBitmapShader.setLocalMatrix(mScaleMatrix); } } /** * Updates the clip rect based on the given task bar. */ void updateClipToTaskBar(View taskBar) { mTaskBar = taskBar; int top = (int) Math.max(0, taskBar.getTranslationY() + taskBar.getMeasuredHeight() - 1); mClipRect.set(0, top, getMeasuredWidth(), getMeasuredHeight()); setClipBounds(mClipRect); } /** * Updates the visibility of the the thumbnail. */ void updateThumbnailVisibility(int clipBottom) { boolean invisible = mTaskBar != null && (getHeight() - clipBottom) <= mTaskBar.getHeight(); if (invisible != mInvisible) { mInvisible = invisible; if (!mInvisible) { updateThumbnailPaintFilter(); } invalidate(); } } /** * Sets the dim alpha, only used when we are not using hardware layers. * (see RecentsConfiguration.useHardwareLayers) */ public void setDimAlpha(float dimAlpha) { mDimAlpha = dimAlpha; updateThumbnailPaintFilter(); } /** * Binds the thumbnail view to the task */ //void rebindToTask(Task t) { void rebindToTask(Bitmap thumbnail) { if (thumbnail != null) { setThumbnail(thumbnail); } else { setThumbnail(null); } } /** * Unbinds the thumbnail view from the task */ void unbindFromTask() { setThumbnail(null); } Bitmap mThumbnail; public Bitmap getThumbnail() { return mThumbnail; } /** * Handles focus changes. */ void onFocusChanged(boolean focused) { if (focused) { if (Float.compare(getAlpha(), 1f) != 0) { startFadeAnimation(1f, 0, 150, null); } } else { if (Float.compare(getAlpha(), mConfig.taskViewThumbnailAlpha) != 0) { startFadeAnimation(mConfig.taskViewThumbnailAlpha, 0, 150, null); } } } /** * Prepares for the enter recents animation, this gets called before the the view * is first visible and will be followed by a startEnterRecentsAnimation() call. */ void prepareEnterRecentsAnimation(boolean isTaskViewLaunchTargetTask) { if (isTaskViewLaunchTargetTask) { mThumbnailAlpha = 1f; } else { mThumbnailAlpha = mConfig.taskViewThumbnailAlpha; } updateThumbnailPaintFilter(); } /** * Animates this task thumbnail as it enters Recents. */ void startEnterRecentsAnimation(int delay, Runnable postAnimRunnable) { startFadeAnimation(mConfig.taskViewThumbnailAlpha, delay, mConfig.taskViewEnterFromAppDuration, postAnimRunnable); } /** * Animates this task thumbnail as it exits Recents. */ void startLaunchTaskAnimation(Runnable postAnimRunnable) { startFadeAnimation(1f, 0, mConfig.taskViewExitToAppDuration, postAnimRunnable); } /** * Starts a new thumbnail alpha animation. */ void startFadeAnimation(float finalAlpha, int delay, int duration, final Runnable postAnimRunnable) { DVUtils.cancelAnimationWithoutCallbacks(mThumbnailAlphaAnimator); mThumbnailAlphaAnimator = ValueAnimator.ofFloat(mThumbnailAlpha, finalAlpha); mThumbnailAlphaAnimator.setStartDelay(delay); mThumbnailAlphaAnimator.setDuration(duration); mThumbnailAlphaAnimator.setInterpolator(mConfig.fastOutSlowInInterpolator); mThumbnailAlphaAnimator.addUpdateListener(mThumbnailAlphaUpdateListener); if (postAnimRunnable != null) { mThumbnailAlphaAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { postAnimRunnable.run(); } }); } mThumbnailAlphaAnimator.start(); } } ================================================ FILE: deckview/src/main/java/com/appeaser/deckview/views/DeckView.java ================================================ package com.appeaser.deckview.views; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Matrix; import android.graphics.Rect; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.WindowInsets; import android.view.accessibility.AccessibilityEvent; import android.widget.FrameLayout; import com.appeaser.deckview.R; import com.appeaser.deckview.helpers.DeckChildViewTransform; import com.appeaser.deckview.helpers.DeckViewConfig; import com.appeaser.deckview.utilities.DVConstants; import com.appeaser.deckview.utilities.DVUtils; import com.appeaser.deckview.utilities.DozeTrigger; import com.appeaser.deckview.utilities.ReferenceCountedTrigger; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; /** * Created by Vikram on 02/04/2015. */ /* The visual representation of a task stack view */ public class DeckView extends FrameLayout implements /*TaskStack.TaskStackCallbacks,*/ DeckChildView.DeckChildViewCallbacks, DeckViewScroller.DeckViewScrollerCallbacks, ViewPool.ViewPoolConsumer, T> { DeckViewConfig mConfig; DeckViewLayoutAlgorithm mLayoutAlgorithm; DeckViewScroller mStackScroller; DeckViewTouchHandler mTouchHandler; ViewPool, T> mViewPool; ArrayList mCurrentTaskTransforms = new ArrayList(); DozeTrigger mUIDozeTrigger; Rect mTaskStackBounds = new Rect(); int mFocusedTaskIndex = -1; int mPrevAccessibilityFocusedIndex = -1; // Optimizations int mStackViewsAnimationDuration; boolean mStackViewsDirty = true; boolean mStackViewsClipDirty = true; boolean mAwaitingFirstLayout = true; boolean mStartEnterAnimationRequestedAfterLayout; boolean mStartEnterAnimationCompleted; ViewAnimation.TaskViewEnterContext mStartEnterAnimationContext; int[] mTmpVisibleRange = new int[2]; float[] mTmpCoord = new float[2]; Matrix mTmpMatrix = new Matrix(); Rect mTmpRect = new Rect(); DeckChildViewTransform mTmpTransform = new DeckChildViewTransform(); HashMap mTmpTaskViewMap = new HashMap(); LayoutInflater mInflater; // A convenience update listener to request updating clipping of tasks ValueAnimator.AnimatorUpdateListener mRequestUpdateClippingListener = new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { requestUpdateStackViewsClip(); } }; public DeckView(Context context) { this(context, null); } public DeckView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public DeckView(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public DeckView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); DeckViewConfig.reinitialize(getContext()); mConfig = DeckViewConfig.getInstance(); } public void initialize(Callback callback) { mCallback = callback; requestLayout(); mViewPool = new ViewPool, T>(getContext(), this); mInflater = LayoutInflater.from(getContext()); mLayoutAlgorithm = new DeckViewLayoutAlgorithm(mConfig); mStackScroller = new DeckViewScroller(getContext(), mConfig, mLayoutAlgorithm); mStackScroller.setCallbacks(this); mTouchHandler = new DeckViewTouchHandler(getContext(), this, mConfig, mStackScroller); mUIDozeTrigger = new DozeTrigger(mConfig.taskBarDismissDozeDelaySeconds, new Runnable() { @Override public void run() { // Show the task bar dismiss buttons int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { DeckChildView tv = (DeckChildView) getChildAt(i); tv.startNoUserInteractionAnimation(); } } }); } /** * Resets this TaskStackView for reuse. */ void reset() { // Reset the focused task resetFocusedTask(); // Return all the views to the pool int childCount = getChildCount(); for (int i = childCount - 1; i >= 0; i--) { DeckChildView tv = (DeckChildView) getChildAt(i); mViewPool.returnViewToPool(tv); } // Mark each task view for relayout if (mViewPool != null) { Iterator> iter = mViewPool.poolViewIterator(); if (iter != null) { while (iter.hasNext()) { DeckChildView tv = iter.next(); tv.reset(); } } } // Reset the stack state mStackViewsDirty = true; mStackViewsClipDirty = true; mAwaitingFirstLayout = true; mPrevAccessibilityFocusedIndex = -1; if (mUIDozeTrigger != null) { mUIDozeTrigger.stopDozing(); mUIDozeTrigger.resetTrigger(); } mStackScroller.reset(); } /** * Requests that the views be synchronized with the model */ void requestSynchronizeStackViewsWithModel() { requestSynchronizeStackViewsWithModel(0); } void requestSynchronizeStackViewsWithModel(int duration) { if (!mStackViewsDirty) { invalidate(); mStackViewsDirty = true; } if (mAwaitingFirstLayout) { // Skip the animation if we are awaiting first layout mStackViewsAnimationDuration = 0; } else { mStackViewsAnimationDuration = Math.max(mStackViewsAnimationDuration, duration); } } /** * Requests that the views clipping be updated. */ void requestUpdateStackViewsClip() { if (!mStackViewsClipDirty) { invalidate(); mStackViewsClipDirty = true; } } /** * Finds the child view given a specific task. */ public DeckChildView getChildViewForTask(T key) { int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { DeckChildView tv = (DeckChildView) getChildAt(i); if (tv.getAttachedKey().equals(key)) { return tv; } } return null; } /** * Returns the stack algorithm for this task stack. */ public DeckViewLayoutAlgorithm getStackAlgorithm() { return mLayoutAlgorithm; } /** * Gets the stack transforms of a list of tasks, and returns the visible range of tasks. */ private boolean updateStackTransforms(ArrayList taskTransforms, ArrayList data, float stackScroll, int[] visibleRangeOut, boolean boundTranslationsToRect) { int taskTransformCount = taskTransforms.size(); int taskCount = data.size(); int frontMostVisibleIndex = -1; int backMostVisibleIndex = -1; // We can reuse the task transforms where possible to reduce object allocation if (taskTransformCount < taskCount) { // If there are less transforms than tasks, then add as many transforms as necessary for (int i = taskTransformCount; i < taskCount; i++) { taskTransforms.add(new DeckChildViewTransform()); } } else if (taskTransformCount > taskCount) { // If there are more transforms than tasks, then just subset the transform list taskTransforms.subList(0, taskCount); } // Update the stack transforms DeckChildViewTransform prevTransform = null; for (int i = taskCount - 1; i >= 0; i--) { DeckChildViewTransform transform = mLayoutAlgorithm.getStackTransform(data.get(i), stackScroll, taskTransforms.get(i), prevTransform); if (transform.visible) { if (frontMostVisibleIndex < 0) { frontMostVisibleIndex = i; } backMostVisibleIndex = i; } else { if (backMostVisibleIndex != -1) { // We've reached the end of the visible range, so going down the rest of the // stack, we can just reset the transforms accordingly while (i >= 0) { taskTransforms.get(i).reset(); i--; } break; } } if (boundTranslationsToRect) { transform.translationY = Math.min(transform.translationY, mLayoutAlgorithm.mViewRect.bottom); } prevTransform = transform; } if (visibleRangeOut != null) { visibleRangeOut[0] = frontMostVisibleIndex; visibleRangeOut[1] = backMostVisibleIndex; } return frontMostVisibleIndex != -1 && backMostVisibleIndex != -1; } /** * Synchronizes the views with the model */ boolean synchronizeStackViewsWithModel() { if (mStackViewsDirty) { // Get all the task transforms ArrayList data = mCallback.getData(); float stackScroll = mStackScroller.getStackScroll(); int[] visibleRange = mTmpVisibleRange; boolean isValidVisibleRange = updateStackTransforms(mCurrentTaskTransforms, data, stackScroll, visibleRange, false); // Return all the invisible children to the pool mTmpTaskViewMap.clear(); int childCount = getChildCount(); for (int i = childCount - 1; i >= 0; i--) { DeckChildView tv = (DeckChildView) getChildAt(i); T key = tv.getAttachedKey(); int taskIndex = data.indexOf(key); if (visibleRange[1] <= taskIndex && taskIndex <= visibleRange[0]) { mTmpTaskViewMap.put(key, tv); } else { mViewPool.returnViewToPool(tv); } } for (int i = visibleRange[0]; isValidVisibleRange && i >= visibleRange[1]; i--) { T key = data.get(i); DeckChildViewTransform transform = mCurrentTaskTransforms.get(i); DeckChildView tv = mTmpTaskViewMap.get(key); if (tv == null) { // TODO Check tv = mViewPool.pickUpViewFromPool(key, key); if (mStackViewsAnimationDuration > 0) { // For items in the list, put them in start animating them from the // approriate ends of the list where they are expected to appear if (Float.compare(transform.p, 0f) <= 0) { mLayoutAlgorithm.getStackTransform(0f, 0f, mTmpTransform, null); } else { mLayoutAlgorithm.getStackTransform(1f, 0f, mTmpTransform, null); } tv.updateViewPropertiesToTaskTransform(mTmpTransform, 0); } } // Animate the task into place tv.updateViewPropertiesToTaskTransform(mCurrentTaskTransforms.get(i), mStackViewsAnimationDuration, mRequestUpdateClippingListener); } // Reset the request-synchronize params mStackViewsAnimationDuration = 0; mStackViewsDirty = false; mStackViewsClipDirty = true; return true; } return false; } /** * Updates the clip for each of the task views. */ void clipTaskViews() { // Update the clip on each task child if (DVConstants.DebugFlags.App.EnableTaskStackClipping) { int childCount = getChildCount(); for (int i = 0; i < childCount - 1; i++) { DeckChildView tv = (DeckChildView) getChildAt(i); DeckChildView nextTv = null; DeckChildView tmpTv = null; int clipBottom = 0; if (tv.shouldClipViewInStack()) { // Find the next view to clip against int nextIndex = i; while (nextIndex < getChildCount()) { tmpTv = (DeckChildView) getChildAt(++nextIndex); if (tmpTv != null && tmpTv.shouldClipViewInStack()) { nextTv = tmpTv; break; } } // Clip against the next view, this is just an approximation since we are // stacked and we can make assumptions about the visibility of the this // task relative to the ones in front of it. if (nextTv != null) { // Map the top edge of next task view into the local space of the current // task view to find the clip amount in local space mTmpCoord[0] = mTmpCoord[1] = 0; DVUtils.mapCoordInDescendentToSelf(nextTv, this, mTmpCoord, false); DVUtils.mapCoordInSelfToDescendent(tv, this, mTmpCoord, mTmpMatrix); clipBottom = (int) Math.floor(tv.getMeasuredHeight() - mTmpCoord[1] - nextTv.getPaddingTop() - 1); } } tv.getViewBounds().setClipBottom(clipBottom); } if (getChildCount() > 0) { // The front most task should never be clipped DeckChildView tv = (DeckChildView) getChildAt(getChildCount() - 1); tv.getViewBounds().setClipBottom(0); } } mStackViewsClipDirty = false; } /** * The stack insets to apply to the stack contents */ public void setStackInsetRect(Rect r) { mTaskStackBounds.set(r); } /** * Updates the min and max virtual scroll bounds */ void updateMinMaxScroll(boolean boundScrollToNewMinMax, boolean launchedWithAltTab, boolean launchedFromHome) { // Compute the min and max scroll values mLayoutAlgorithm.computeMinMaxScroll(mCallback.getData(), launchedWithAltTab, launchedFromHome); // Debug logging if (boundScrollToNewMinMax) { mStackScroller.boundScroll(); } } /** * Returns the scroller. */ public DeckViewScroller getScroller() { return mStackScroller; } /** * Focuses the task at the specified index in the stack */ void focusTask(int childIndex, boolean scrollToNewPosition, final boolean animateFocusedState) { // Return early if the task is already focused if (childIndex == mFocusedTaskIndex) return; ArrayList data = mCallback.getData(); if (0 <= childIndex && childIndex < data.size()) { mFocusedTaskIndex = childIndex; // Focus the view if possible, otherwise, focus the view after we scroll into position T key = data.get(childIndex); DeckChildView tv = getChildViewForTask(key); Runnable postScrollRunnable = null; if (tv != null) { tv.setFocusedTask(animateFocusedState); } else { postScrollRunnable = new Runnable() { @Override public void run() { DeckChildView tv = getChildViewForTask(mCallback.getData().get(mFocusedTaskIndex)); if (tv != null) { tv.setFocusedTask(animateFocusedState); } } }; } // Scroll the view into position (just center it in the curve) if (scrollToNewPosition) { float newScroll = mLayoutAlgorithm.getStackScrollForTask(key) - 0.5f; newScroll = mStackScroller.getBoundedStackScroll(newScroll); mStackScroller.animateScroll(mStackScroller.getStackScroll(), newScroll, postScrollRunnable); } else { if (postScrollRunnable != null) { postScrollRunnable.run(); } } } } /** * Ensures that there is a task focused, if nothing is focused, then we will use the task * at the center of the visible stack. */ public boolean ensureFocusedTask() { if (mFocusedTaskIndex < 0) { // If there is no task focused, then find the task that is closes to the center // of the screen and use that as the currently focused task int x = mLayoutAlgorithm.mStackVisibleRect.centerX(); int y = mLayoutAlgorithm.mStackVisibleRect.centerY(); int childCount = getChildCount(); for (int i = childCount - 1; i >= 0; i--) { DeckChildView tv = (DeckChildView) getChildAt(i); tv.getHitRect(mTmpRect); if (mTmpRect.contains(x, y)) { mFocusedTaskIndex = i; break; } } // If we can't find the center task, then use the front most index if (mFocusedTaskIndex < 0 && childCount > 0) { mFocusedTaskIndex = childCount - 1; } } return mFocusedTaskIndex >= 0; } /** * Focuses the next task in the stack. * * @param animateFocusedState determines whether to actually draw the highlight along with * the change in focus, as well as whether to scroll to fit the * task into view. */ public void focusNextTask(boolean forward, boolean animateFocusedState) { // Find the next index to focus int numTasks = mCallback.getData().size(); if (numTasks == 0) return; int direction = (forward ? -1 : 1); int newIndex = mFocusedTaskIndex + direction; if (newIndex >= 0 && newIndex <= (numTasks - 1)) { newIndex = Math.max(0, Math.min(numTasks - 1, newIndex)); focusTask(newIndex, true, animateFocusedState); } } /** * Dismisses the focused task. */ public void dismissFocusedTask() { // Return early if the focused task index is invalid if (mFocusedTaskIndex < 0 || mFocusedTaskIndex >= mCallback.getData().size()) { mFocusedTaskIndex = -1; return; } //Long id = mAdapter.getItemId(mFocusedTaskIndex); T key = mCallback.getData().get(mFocusedTaskIndex); DeckChildView tv = getChildViewForTask(key); tv.dismissTask(); } /** * Resets the focused task. */ void resetFocusedTask() { if ((0 <= mFocusedTaskIndex) && (mFocusedTaskIndex < mCallback.getData().size())) { DeckChildView tv = getChildViewForTask(mCallback.getData().get(mFocusedTaskIndex)); if (tv != null) { tv.unsetFocusedTask(); } } mFocusedTaskIndex = -1; } @Override public void onInitializeAccessibilityEvent(AccessibilityEvent event) { super.onInitializeAccessibilityEvent(event); int childCount = getChildCount(); if (childCount > 0) { DeckChildView backMostTask = (DeckChildView) getChildAt(0); DeckChildView frontMostTask = (DeckChildView) getChildAt(childCount - 1); event.setFromIndex(mCallback.getData().indexOf(backMostTask.getAttachedKey())); event.setToIndex(mCallback.getData().indexOf(frontMostTask.getAttachedKey())); } event.setItemCount(mCallback.getData().size()); event.setScrollY(mStackScroller.mScroller.getCurrY()); event.setMaxScrollY(mStackScroller.progressToScrollRange(mLayoutAlgorithm.mMaxScrollP)); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { return mTouchHandler.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent ev) { return mTouchHandler.onTouchEvent(ev); } @Override public boolean onGenericMotionEvent(MotionEvent ev) { return mTouchHandler.onGenericMotionEvent(ev); } @Override public void computeScroll() { mStackScroller.computeScroll(); // Synchronize the views synchronizeStackViewsWithModel(); clipTaskViews(); // Notify accessibility sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SCROLLED); } /** * Computes the stack and task rects */ public void computeRects(int windowWidth, int windowHeight, Rect taskStackBounds, boolean launchedWithAltTab, boolean launchedFromHome) { // Compute the rects in the stack algorithm mLayoutAlgorithm.computeRects(windowWidth, windowHeight, taskStackBounds); // Update the scroll bounds updateMinMaxScroll(false, launchedWithAltTab, launchedFromHome); } public int getCurrentChildIndex() { if (getChildCount() == 0) return -1; DeckChildView frontMostChild = (DeckChildView) getChildAt(getChildCount() / 2); if (frontMostChild != null) { return mCallback.getData().indexOf(frontMostChild.getAttachedKey()); } return -1; } /** * Focuses the task at the specified index in the stack */ public void scrollToChild(int childIndex) { if (getCurrentChildIndex() == childIndex) return; if (0 <= childIndex && childIndex < mCallback.getData().size()) { // Scroll the view into position (just center it in the curve) float newScroll = mLayoutAlgorithm.getStackScrollForTask( mCallback.getData().get(childIndex)) - 0.5f; newScroll = mStackScroller.getBoundedStackScroll(newScroll); mStackScroller.setStackScroll(newScroll); //Alternate (animated) way //mStackScroller.animateScroll(mStackScroller.getStackScroll(), newScroll, null); } } /** * Computes the maximum number of visible tasks and thumbnails. Requires that * updateMinMaxScrollForStack() is called first. */ public DeckViewLayoutAlgorithm.VisibilityReport computeStackVisibilityReport() { return mLayoutAlgorithm.computeStackVisibilityReport(mCallback.getData()); } /** * This is called with the full window width and height to allow stack view children to * perform the full screen transition down. */ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); Rect _taskStackBounds = new Rect(); mConfig.getTaskStackBounds(width, height, mConfig.systemInsets.top, mConfig.systemInsets.right, _taskStackBounds); setStackInsetRect(_taskStackBounds); // Compute our stack/task rects Rect taskStackBounds = new Rect(mTaskStackBounds); taskStackBounds.bottom -= mConfig.systemInsets.bottom; computeRects(width, height, taskStackBounds, mConfig.launchedWithAltTab, mConfig.launchedFromHome); // If this is the first layout, then scroll to the front of the stack and synchronize the // stack views immediately to load all the views if (mAwaitingFirstLayout) { mStackScroller.setStackScrollToInitialState(); requestSynchronizeStackViewsWithModel(); synchronizeStackViewsWithModel(); } // Measure each of the TaskViews int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { DeckChildView tv = (DeckChildView) getChildAt(i); if (tv.getBackground() != null) { tv.getBackground().getPadding(mTmpRect); } else { mTmpRect.setEmpty(); } tv.measure( MeasureSpec.makeMeasureSpec( mLayoutAlgorithm.mTaskRect.width() + mTmpRect.left + mTmpRect.right, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec( mLayoutAlgorithm.mTaskRect.height() + mTmpRect.top + mTmpRect.bottom, MeasureSpec.EXACTLY)); } setMeasuredDimension(width, height); } /** * This is called with the size of the space not including the top or right insets, or the * search bar height in portrait (but including the search bar width in landscape, since we want * to draw under it. */ @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { // Layout each of the children int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { DeckChildView tv = (DeckChildView) getChildAt(i); if (tv.getBackground() != null) { tv.getBackground().getPadding(mTmpRect); } else { mTmpRect.setEmpty(); } tv.layout(mLayoutAlgorithm.mTaskRect.left - mTmpRect.left, mLayoutAlgorithm.mTaskRect.top - mTmpRect.top, mLayoutAlgorithm.mTaskRect.right + mTmpRect.right, mLayoutAlgorithm.mTaskRect.bottom + mTmpRect.bottom); } if (mAwaitingFirstLayout) { mAwaitingFirstLayout = false; onFirstLayout(); } } /** * Handler for the first layout. */ void onFirstLayout() { int offscreenY = mLayoutAlgorithm.mViewRect.bottom - (mLayoutAlgorithm.mTaskRect.top - mLayoutAlgorithm.mViewRect.top); int childCount = getChildCount(); // Prepare the first view for its enter animation for (int i = childCount - 1; i >= 0; i--) { DeckChildView tv = (DeckChildView) getChildAt(i); // TODO: The false needs to go! tv.prepareEnterRecentsAnimation(i == childCount - 1, false, offscreenY); } // If the enter animation started already and we haven't completed a layout yet, do the // enter animation now if (mStartEnterAnimationRequestedAfterLayout) { startEnterRecentsAnimation(mStartEnterAnimationContext); mStartEnterAnimationRequestedAfterLayout = false; mStartEnterAnimationContext = null; } // When Alt-Tabbing, focus the previous task (but leave the animation until we finish the // enter animation). if (mConfig.launchedWithAltTab) { if (mConfig.launchedFromAppWithThumbnail) { focusTask(Math.max(0, mCallback.getData().size() - 2), false, mConfig.launchedHasConfigurationChanged); } else { focusTask(Math.max(0, mCallback.getData().size() - 1), false, mConfig.launchedHasConfigurationChanged); } } // Start dozing mUIDozeTrigger.startDozing(); } void showDeck(Context context) { // Try and start the enter animation (or restart it on configuration changed) ReferenceCountedTrigger t = new ReferenceCountedTrigger(context, null, null, null); ViewAnimation.TaskViewEnterContext ctx = new ViewAnimation.TaskViewEnterContext(t); // We have to increment/decrement the post animation trigger in case there are no children // to ensure that it runs ctx.postAnimationTrigger.increment(); startEnterRecentsAnimation(ctx); ctx.postAnimationTrigger.decrement(); } /** * Requests this task stacks to start it's enter-recents animation */ public void startEnterRecentsAnimation(ViewAnimation.TaskViewEnterContext ctx) { // If we are still waiting to layout, then just defer until then if (mAwaitingFirstLayout) { mStartEnterAnimationRequestedAfterLayout = true; mStartEnterAnimationContext = ctx; return; } if (mCallback.getData().size() > 0) { int childCount = getChildCount(); // Animate all the task views into view for (int i = childCount - 1; i >= 0; i--) { DeckChildView tv = (DeckChildView) getChildAt(i); T key = tv.getAttachedKey(); ctx.currentTaskTransform = new DeckChildViewTransform(); ctx.currentStackViewIndex = i; ctx.currentStackViewCount = childCount; ctx.currentTaskRect = mLayoutAlgorithm.mTaskRect; // TODO: this needs to go ctx.currentTaskOccludesLaunchTarget = false; ctx.updateListener = mRequestUpdateClippingListener; mLayoutAlgorithm.getStackTransform(key, mStackScroller.getStackScroll(), ctx.currentTaskTransform, null); tv.startEnterRecentsAnimation(ctx); } // Add a runnable to the post animation ref counter to clear all the views ctx.postAnimationTrigger.addLastDecrementRunnable(new Runnable() { @Override public void run() { mStartEnterAnimationCompleted = true; // Poke the dozer to restart the trigger after the animation completes mUIDozeTrigger.poke(); } }); } } @Override public WindowInsets onApplyWindowInsets(WindowInsets insets) { // Update the configuration with the latest system insets and trigger a relayout // mConfig.updateSystemInsets(insets.getSystemWindowInsets()); mConfig.updateSystemInsets(new Rect(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom())); requestLayout(); return insets.consumeSystemWindowInsets(); } void hideDeck(Context context, Runnable finishRunnable) { ReferenceCountedTrigger exitTrigger = new ReferenceCountedTrigger(context, null, finishRunnable, null); ViewAnimation.TaskViewExitContext exitCtx = new ViewAnimation.TaskViewExitContext(exitTrigger); exitCtx.postAnimationTrigger.increment(); startExitToHomeAnimation( new ViewAnimation.TaskViewExitContext(exitTrigger)); exitCtx.postAnimationTrigger.decrement(); } /** * Requests this task stacks to start it's exit-recents animation. */ public void startExitToHomeAnimation(ViewAnimation.TaskViewExitContext ctx) { // Stop any scrolling mStackScroller.stopScroller(); mStackScroller.stopBoundScrollAnimation(); // Animate all the task views out of view ctx.offscreenTranslationY = mLayoutAlgorithm.mViewRect.bottom - (mLayoutAlgorithm.mTaskRect.top - mLayoutAlgorithm.mViewRect.top); int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { DeckChildView tv = (DeckChildView) getChildAt(i); tv.startExitToHomeAnimation(ctx); } } /** * Animates a task view in this stack as it launches. */ public void startLaunchTaskAnimation(DeckChildView tv, Runnable r, boolean lockToTask) { int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { DeckChildView t = (DeckChildView) getChildAt(i); if (t == tv) { t.setClipViewInStack(false); t.startLaunchTaskAnimation(r, true, true, lockToTask); } else { // TODO: the false needs to go t.startLaunchTaskAnimation(null, false, false, lockToTask); } } } /** * Final callback after Recents is finally hidden. */ void onRecentsHidden() { reset(); } public boolean isTransformedTouchPointInView(float x, float y, View child) { // TODO: confirm if this is the right approach if (child == null) return false; final Rect frame = new Rect(); child.getHitRect(frame); return frame.contains((int) x, (int) y); } /** * Pokes the dozer on user interaction. */ void onUserInteraction() { // Poke the doze trigger if it is dozing mUIDozeTrigger.poke(); } /** * * ViewPoolConsumer Implementation *** */ @Override public DeckChildView createView(Context context) { return (DeckChildView) mInflater.inflate(R.layout.deck_child_view, this, false); } @Override public void prepareViewToEnterPool(DeckChildView tv) { T key = tv.getAttachedKey(); mCallback.unloadViewData(key); tv.onTaskUnbound(); tv.onDataUnloaded(); // Detach the view from the hierarchy detachViewFromParent(tv); // Reset the view properties tv.resetViewProperties(); // Reset the clip state of the task view tv.setClipViewInStack(false); } @Override public void prepareViewToLeavePool(DeckChildView dcv, T key, boolean isNewView) { // It is possible for a view to be returned to the view pool before it is laid out, // which means that we will need to relayout the view when it is first used next. boolean requiresRelayout = dcv.getWidth() <= 0 && !isNewView; // Rebind the task and request that this task's data be filled into the TaskView dcv.onTaskBound(key); // Load the task data mCallback.loadViewData(new WeakReference>(dcv), key); // If the doze trigger has already fired, then update the state for this task view if (mUIDozeTrigger.hasTriggered()) { dcv.setNoUserInteractionState(); } // If we've finished the start animation, then ensure we always enable the focus animations if (mStartEnterAnimationCompleted) { dcv.enableFocusAnimations(); } // Find the index where this task should be placed in the stack int insertIndex = -1; int position = mCallback.getData().indexOf(key); if (position != -1) { int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { T otherKey = ((DeckChildView) getChildAt(i)).getAttachedKey(); int pos = mCallback.getData().indexOf(otherKey); if (position < pos) { insertIndex = i; break; } } } // Add/attach the view to the hierarchy if (isNewView) { addView(dcv, insertIndex); } else { attachViewToParent(dcv, insertIndex, dcv.getLayoutParams()); if (requiresRelayout) { dcv.requestLayout(); } } // Set the new state for this view, including the callbacks and view clipping dcv.setCallbacks(this); dcv.setTouchEnabled(true); dcv.setClipViewInStack(true); } @Override public boolean hasPreferredData(DeckChildView tv, T preferredData) { return (tv.getAttachedKey() != null && tv.getAttachedKey().equals(preferredData)); } /** * * DeckChildCallbacks Implementation *** */ @Override public void onDeckChildViewAppIconClicked(DeckChildView tv) { // } @Override public void onDeckChildViewAppInfoClicked(DeckChildView tv) { // } @Override public void onDeckChildViewClicked(DeckChildView dcv, T key) { // Cancel any doze triggers mUIDozeTrigger.stopDozing(); mCallback.onItemClick(key); } @Override public void onDeckChildViewDismissed(DeckChildView dcv) { boolean taskWasFocused = dcv.isFocusedTask(); T key = dcv.getAttachedKey(); int taskIndex = mCallback.getData().indexOf(key); onStackTaskRemoved(dcv); // If the dismissed task was focused, then we should focus the new task in the same index if (taskIndex != -1 && taskWasFocused) { int nextTaskIndex = Math.min(mCallback.getData().size() - 1, taskIndex - 1); if (nextTaskIndex >= 0) { DeckChildView nextTv = getChildViewForTask(mCallback.getData().get(nextTaskIndex)); if (nextTv != null) { // Focus the next task, and only animate the visible state if we are launched // from Alt-Tab nextTv.setFocusedTask(mConfig.launchedWithAltTab); } } } } public void onStackTaskRemoved(DeckChildView removedView) { // Remove the view associated with this task, we can't rely on updateTransforms // to work here because the task is no longer in the list if (removedView != null) { T key = removedView.getAttachedKey(); int removedPosition = mCallback.getData().indexOf(key); mViewPool.returnViewToPool(removedView); // Notify the callback that we've removed the task and it can clean up after it mCallback.onViewDismissed(key); } /* // Get the stack scroll of the task to anchor to (since we are removing something, the front // most task will be our anchor task) T anchorTask = null; float prevAnchorTaskScroll = 0; boolean pullStackForward = mCallback.getData().size() > 0; if (pullStackForward) { anchorTask = mCallback.getData().get(mCallback.getData().size() - 1); prevAnchorTaskScroll = mLayoutAlgorithm.getStackScrollForTask(anchorTask); } // Update the min/max scroll and animate other task views into their new positions updateMinMaxScroll(true, mConfig.launchedWithAltTab, mConfig.launchedFromHome); // Offset the stack by as much as the anchor task would otherwise move back if (pullStackForward) { float anchorTaskScroll = mLayoutAlgorithm.getStackScrollForTask(anchorTask); mStackScroller.setStackScroll(mStackScroller.getStackScroll() + (anchorTaskScroll - prevAnchorTaskScroll)); mStackScroller.boundScroll(); } // Animate all the tasks into place requestSynchronizeStackViewsWithModel(200); T newFrontMostTask = mCallback.getData().get(mCallback.getData().size() - 1); // Update the new front most task if (newFrontMostTask != null) { DeckChildView frontTv = getChildViewForTask(newFrontMostTask); if (frontTv != null) { frontTv.onTaskBound(newFrontMostTask); } } // If there are no remaining tasks if (mCallback.getData().size() == 0) { mCallback.onNoViewsToDeck(); } */ } public void notifyDataSetChanged() { // Get the stack scroll of the task to anchor to (since we are removing something, the front // most task will be our anchor task) T anchorTask = null; float prevAnchorTaskScroll = 0; boolean pullStackForward = mCallback.getData().size() > 0; if (pullStackForward) { anchorTask = mCallback.getData().get(mCallback.getData().size() - 1); prevAnchorTaskScroll = mLayoutAlgorithm.getStackScrollForTask(anchorTask); } // Update the min/max scroll and animate other task views into their new positions updateMinMaxScroll(true, mConfig.launchedWithAltTab, mConfig.launchedFromHome); // Offset the stack by as much as the anchor task would otherwise move back if (pullStackForward) { float anchorTaskScroll = mLayoutAlgorithm.getStackScrollForTask(anchorTask); mStackScroller.setStackScroll(mStackScroller.getStackScroll() + (anchorTaskScroll - prevAnchorTaskScroll)); mStackScroller.boundScroll(); } // Animate all the tasks into place requestSynchronizeStackViewsWithModel(200); T newFrontMostTask = mCallback.getData().size() > 0 ? mCallback.getData().get(mCallback.getData().size() - 1) : null; // Update the new front most task if (newFrontMostTask != null) { DeckChildView frontTv = getChildViewForTask(newFrontMostTask); if (frontTv != null) { frontTv.onTaskBound(newFrontMostTask); } } // If there are no remaining tasks if (mCallback.getData().size() == 0) { mCallback.onNoViewsToDeck(); } } @Override public void onDeckChildViewClipStateChanged(DeckChildView tv) { if (!mStackViewsDirty) { invalidate(); } } @Override public void onDeckChildViewFocusChanged(DeckChildView tv, boolean focused) { if (focused) { mFocusedTaskIndex = mCallback.getData().indexOf(tv.getAttachedKey()); } } /** * * TaskStackViewScroller.TaskStackViewScrollerCallbacks *** */ @Override public void onScrollChanged(float p) { mUIDozeTrigger.poke(); requestSynchronizeStackViewsWithModel(); postInvalidateOnAnimation(); } public void notifyDataSetChangedOld() { ArrayList data = mCallback.getData(); // Get the stack scroll of the task to anchor to (since we are removing something, the front // most task will be our anchor task) T anchorTask = null; float prevAnchorTaskScroll = 0; boolean pullStackForward = data.size() > 0; if (pullStackForward) { anchorTask = data.get(data.size() - 1); prevAnchorTaskScroll = mLayoutAlgorithm.getStackScrollForTask(anchorTask); } // Update the min/max scroll and animate other task views into their new positions updateMinMaxScroll(true, mConfig.launchedWithAltTab, mConfig.launchedFromHome); // Offset the stack by as much as the anchor task would otherwise move back if (pullStackForward) { float anchorTaskScroll = mLayoutAlgorithm.getStackScrollForTask(anchorTask); mStackScroller.setStackScroll(mStackScroller.getStackScroll() + (anchorTaskScroll - prevAnchorTaskScroll)); mStackScroller.boundScroll(); } // Animate all the tasks into place requestSynchronizeStackViewsWithModel(200); T newFrontMostTask = data.get(data.size() - 1); // Update the new front most task if (newFrontMostTask != null) { DeckChildView frontTv = getChildViewForTask(newFrontMostTask); if (frontTv != null) { frontTv.onTaskBound(newFrontMostTask); } } // If there are no remaining tasks if (mCallback.getData().size() == 0) mCallback.onNoViewsToDeck(); } Callback mCallback; public interface Callback { public ArrayList getData(); public void loadViewData(WeakReference> dcv, T item); public void unloadViewData(T item); public void onViewDismissed(T item); public void onItemClick(T item); public void onNoViewsToDeck(); } } ================================================ FILE: deckview/src/main/java/com/appeaser/deckview/views/DeckViewLayoutAlgorithm.java ================================================ package com.appeaser.deckview.views; import android.graphics.Rect; import com.appeaser.deckview.helpers.DeckChildViewTransform; import com.appeaser.deckview.helpers.DeckViewConfig; import com.appeaser.deckview.utilities.DVUtils; import java.util.ArrayList; import java.util.HashMap; /** * Created by Vikram on 02/04/2015. */ /* The layout logic for a TaskStackView. * * We are using a curve that defines the curve of the tasks as that go back in the recents list. * The curve is defined such that at curve progress p = 0 is the end of the curve (the top of the * stack rect), and p = 1 at the start of the curve and the bottom of the stack rect. */ public class DeckViewLayoutAlgorithm { // These are all going to change static final float StackPeekMinScale = 0.8f; // The min scale of the last card in the peek area // A report of the visibility state of the stack public class VisibilityReport { public int numVisibleTasks; public int numVisibleThumbnails; /** * Package level ctor */ VisibilityReport(int tasks, int thumbnails) { numVisibleTasks = tasks; numVisibleThumbnails = thumbnails; } } DeckViewConfig mConfig; // The various rects that define the stack view public Rect mViewRect = new Rect(); Rect mStackVisibleRect = new Rect(); Rect mStackRect = new Rect(); Rect mTaskRect = new Rect(); // The min/max scroll progress float mMinScrollP; float mMaxScrollP; float mInitialScrollP; int mWithinAffiliationOffset; int mBetweenAffiliationOffset; HashMap mTaskProgressMap = new HashMap(); // Log function static final float XScale = 1.75f; // The large the XScale, the longer the flat area of the curve static final float LogBase = 3000; static final int PrecisionSteps = 250; static float[] xp; static float[] px; public DeckViewLayoutAlgorithm(DeckViewConfig config) { mConfig = config; // Precompute the path initializeCurve(); } /** * Computes the stack and task rects */ public void computeRects(int windowWidth, int windowHeight, Rect taskStackBounds) { // Compute the stack rects mViewRect.set(0, 0, windowWidth, windowHeight); mStackRect.set(taskStackBounds); mStackVisibleRect.set(taskStackBounds); mStackVisibleRect.bottom = mViewRect.bottom; int widthPadding = (int) (mConfig.taskStackWidthPaddingPct * mStackRect.width()); int heightPadding = mConfig.taskStackTopPaddingPx; mStackRect.inset(widthPadding, heightPadding); // Compute the task rect int size = mStackRect.width(); int left = mStackRect.left + (mStackRect.width() - size) / 2; mTaskRect.set(left, mStackRect.top, left + size, mStackRect.top + size); // Update the affiliation offsets float visibleTaskPct = 0.5f; mWithinAffiliationOffset = mConfig.taskBarHeight; mBetweenAffiliationOffset = (int) (visibleTaskPct * mTaskRect.height()); } /** * Computes the minimum and maximum scroll progress values. This method may be called before * the RecentsConfiguration is set, so we need to pass in the alt-tab state. */ void computeMinMaxScroll(ArrayList data, boolean launchedWithAltTab, boolean launchedFromHome) { // Clear the progress map mTaskProgressMap.clear(); // Return early if we have no tasks if (data.isEmpty()) { mMinScrollP = mMaxScrollP = 0; return; } // Note that we should account for the scale difference of the offsets at the screen bottom int taskHeight = mTaskRect.height(); float pAtBottomOfStackRect = screenYToCurveProgress(mStackVisibleRect.bottom); float pWithinAffiliateTop = screenYToCurveProgress(mStackVisibleRect.bottom - mWithinAffiliationOffset); float scale = curveProgressToScale(pWithinAffiliateTop); int scaleYOffset = (int) (((1f - scale) * taskHeight) / 2); pWithinAffiliateTop = screenYToCurveProgress(mStackVisibleRect.bottom - mWithinAffiliationOffset + scaleYOffset); float pWithinAffiliateOffset = pAtBottomOfStackRect - pWithinAffiliateTop; float pBetweenAffiliateOffset = pAtBottomOfStackRect - screenYToCurveProgress(mStackVisibleRect.bottom - mBetweenAffiliationOffset); float pTaskHeightOffset = pAtBottomOfStackRect - screenYToCurveProgress(mStackVisibleRect.bottom - taskHeight); float pNavBarOffset = pAtBottomOfStackRect - screenYToCurveProgress(mStackVisibleRect.bottom - (mStackVisibleRect.bottom - mStackRect.bottom)); // Update the task offsets float pAtBackMostCardTop = 0.5f; float pAtFrontMostCardTop = pAtBackMostCardTop; int taskCount = data.size(); for (int i = 0; i < taskCount; i++) { //Task task = tasks.get(i); //mTaskProgressMap.put(task.key, pAtFrontMostCardTop); mTaskProgressMap.put(data.get(i), pAtFrontMostCardTop); if (i < (taskCount - 1)) { // Increment the peek height // TODO: Might need adjustments //float pPeek = task.group.isFrontMostTask(task) ? //pBetweenAffiliateOffset : pWithinAffiliateOffset; float pPeek = pBetweenAffiliateOffset; pAtFrontMostCardTop += pPeek; } } mMaxScrollP = pAtFrontMostCardTop - ((1f - pTaskHeightOffset - pNavBarOffset)); mMinScrollP = data.size() == 1 ? Math.max(mMaxScrollP, 0f) : 0f; if (launchedWithAltTab && launchedFromHome) { // Center the top most task, since that will be focused first mInitialScrollP = mMaxScrollP; } else { mInitialScrollP = pAtFrontMostCardTop - 0.825f; } mInitialScrollP = Math.min(mMaxScrollP, Math.max(0, mInitialScrollP)); } /** * Computes the maximum number of visible tasks and thumbnails. Requires that * computeMinMaxScroll() is called first. */ public VisibilityReport computeStackVisibilityReport(ArrayList data) { if (data.size() <= 1) { return new VisibilityReport(1, 1); } // Walk backwards in the task stack and count the number of tasks and visible thumbnails int taskHeight = mTaskRect.height(); int numVisibleTasks = 1; int numVisibleThumbnails = 1; //float progress = mTaskProgressMap.get(tasks.get(tasks.size() - 1).key) - mInitialScrollP; float progress = mTaskProgressMap.get(data.get(data.size() - 1)) - mInitialScrollP; int prevScreenY = curveProgressToScreenY(progress); for (int i = data.size() - 2; i >= 0; i--) { //Task task = tasks.get(i); //progress = mTaskProgressMap.get(task.key) - mInitialScrollP; progress = mTaskProgressMap.get(data.get(i)) - mInitialScrollP; if (progress < 0) { break; } // TODO: Might need adjustments //boolean isFrontMostTaskInGroup = task.group.isFrontMostTask(task); boolean isFrontMostTaskInGroup = true; if (isFrontMostTaskInGroup) { float scaleAtP = curveProgressToScale(progress); int scaleYOffsetAtP = (int) (((1f - scaleAtP) * taskHeight) / 2); int screenY = curveProgressToScreenY(progress) + scaleYOffsetAtP; boolean hasVisibleThumbnail = (prevScreenY - screenY) > mConfig.taskBarHeight; if (hasVisibleThumbnail) { numVisibleThumbnails++; numVisibleTasks++; prevScreenY = screenY; } else { // Once we hit the next front most task that does not have a visible thumbnail, // walk through remaining visible set for (int j = i; j >= 0; j--) { numVisibleTasks++; progress = mTaskProgressMap.get(data.get(i)) - mInitialScrollP; if (progress < 0) { break; } } break; } } else if (!isFrontMostTaskInGroup) { // Affiliated task, no thumbnail numVisibleTasks++; } } return new VisibilityReport(numVisibleTasks, numVisibleThumbnails); } /** * Update/get the transform */ public DeckChildViewTransform getStackTransform(T key, float stackScroll, DeckChildViewTransform transformOut, DeckChildViewTransform prevTransform) { // Return early if we have an invalid index if (!mTaskProgressMap.containsKey(key)) { transformOut.reset(); return transformOut; } return getStackTransform(mTaskProgressMap.get(key), stackScroll, transformOut, prevTransform); } /** * Update/get the transform */ public DeckChildViewTransform getStackTransform(float taskProgress, float stackScroll, DeckChildViewTransform transformOut, DeckChildViewTransform prevTransform) { float pTaskRelative = taskProgress - stackScroll; float pBounded = Math.max(0, Math.min(pTaskRelative, 1f)); // If the task top is outside of the bounds below the screen, then immediately reset it if (pTaskRelative > 1f) { transformOut.reset(); transformOut.rect.set(mTaskRect); return transformOut; } // The check for the top is trickier, since we want to show the next task if it is at all // visible, even if p < 0. if (pTaskRelative < 0f) { if (prevTransform != null && Float.compare(prevTransform.p, 0f) <= 0) { transformOut.reset(); transformOut.rect.set(mTaskRect); return transformOut; } } float scale = curveProgressToScale(pBounded); int scaleYOffset = (int) (((1f - scale) * mTaskRect.height()) / 2); int minZ = mConfig.taskViewTranslationZMinPx; int maxZ = mConfig.taskViewTranslationZMaxPx; transformOut.scale = scale; transformOut.translationY = curveProgressToScreenY(pBounded) - mStackVisibleRect.top - scaleYOffset; transformOut.translationZ = Math.max(minZ, minZ + (pBounded * (maxZ - minZ))); transformOut.rect.set(mTaskRect); transformOut.rect.offset(0, transformOut.translationY); DVUtils.scaleRectAboutCenter(transformOut.rect, transformOut.scale); transformOut.visible = true; transformOut.p = pTaskRelative; return transformOut; } /** * Returns the untransformed task view size. */ public Rect getUntransformedTaskViewSize() { Rect tvSize = new Rect(mTaskRect); tvSize.offsetTo(0, 0); return tvSize; } /** * Returns the scroll to such task top = 1f; */ public float getStackScrollForTask(T key) { if (!mTaskProgressMap.containsKey(key)) return 0f; return mTaskProgressMap.get(key); } /** * Initializes the curve. */ public static void initializeCurve() { if (xp != null && px != null) return; xp = new float[PrecisionSteps + 1]; px = new float[PrecisionSteps + 1]; // Approximate f(x) float[] fx = new float[PrecisionSteps + 1]; float step = 1f / PrecisionSteps; float x = 0; for (int xStep = 0; xStep <= PrecisionSteps; xStep++) { fx[xStep] = logFunc(x); x += step; } // Calculate the arc length for x:1->0 float pLength = 0; float[] dx = new float[PrecisionSteps + 1]; dx[0] = 0; for (int xStep = 1; xStep < PrecisionSteps; xStep++) { dx[xStep] = (float) Math.sqrt(Math.pow(fx[xStep] - fx[xStep - 1], 2) + Math.pow(step, 2)); pLength += dx[xStep]; } // Approximate p(x), a function of cumulative progress with x, normalized to 0..1 float p = 0; px[0] = 0f; px[PrecisionSteps] = 1f; for (int xStep = 1; xStep <= PrecisionSteps; xStep++) { p += Math.abs(dx[xStep] / pLength); px[xStep] = p; } // Given p(x), calculate the inverse function x(p). This assumes that x(p) is also a valid // function. int xStep = 0; p = 0; xp[0] = 0f; xp[PrecisionSteps] = 1f; for (int pStep = 0; pStep < PrecisionSteps; pStep++) { // Walk forward in px and find the x where px <= p && p < px+1 while (xStep < PrecisionSteps) { if (px[xStep] > p) break; xStep++; } // Now, px[xStep-1] <= p < px[xStep] if (xStep == 0) { xp[pStep] = 0; } else { // Find x such that proportionally, x is correct float fraction = (p - px[xStep - 1]) / (px[xStep] - px[xStep - 1]); x = (xStep - 1 + fraction) * step; xp[pStep] = x; } p += step; } } /** * Reverses and scales out x. */ static float reverse(float x) { return (-x * XScale) + 1; } /** * The log function describing the curve. */ static float logFunc(float x) { return 1f - (float) (Math.pow(LogBase, reverse(x))) / (LogBase); } /** * The inverse of the log function describing the curve. */ float invLogFunc(float y) { return (float) (Math.log((1f - reverse(y)) * (LogBase - 1) + 1) / Math.log(LogBase)); } /** * Converts from the progress along the curve to a screen coordinate. */ int curveProgressToScreenY(float p) { if (p < 0 || p > 1) return mStackVisibleRect.top + (int) (p * mStackVisibleRect.height()); float pIndex = p * PrecisionSteps; int pFloorIndex = (int) Math.floor(pIndex); int pCeilIndex = (int) Math.ceil(pIndex); float xFraction = 0; if (pFloorIndex < PrecisionSteps && (pCeilIndex != pFloorIndex)) { float pFraction = (pIndex - pFloorIndex) / (pCeilIndex - pFloorIndex); xFraction = (xp[pCeilIndex] - xp[pFloorIndex]) * pFraction; } float x = xp[pFloorIndex] + xFraction; return mStackVisibleRect.top + (int) (x * mStackVisibleRect.height()); } /** * Converts from the progress along the curve to a scale. */ float curveProgressToScale(float p) { if (p < 0) return StackPeekMinScale; if (p > 1) return 1f; float scaleRange = (1f - StackPeekMinScale); float scale = StackPeekMinScale + (p * scaleRange); return scale; } /** * Converts from a screen coordinate to the progress along the curve. */ float screenYToCurveProgress(int screenY) { float x = (float) (screenY - mStackVisibleRect.top) / mStackVisibleRect.height(); if (x < 0 || x > 1) return x; float xIndex = x * PrecisionSteps; int xFloorIndex = (int) Math.floor(xIndex); int xCeilIndex = (int) Math.ceil(xIndex); float pFraction = 0; if (xFloorIndex < PrecisionSteps && (xCeilIndex != xFloorIndex)) { float xFraction = (xIndex - xFloorIndex) / (xCeilIndex - xFloorIndex); pFraction = (px[xCeilIndex] - px[xFloorIndex]) * xFraction; } return px[xFloorIndex] + pFraction; } } ================================================ FILE: deckview/src/main/java/com/appeaser/deckview/views/DeckViewScroller.java ================================================ package com.appeaser.deckview.views; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; import android.widget.OverScroller; import com.appeaser.deckview.helpers.DeckViewConfig; import com.appeaser.deckview.utilities.DVUtils; /** * Created by Vikram on 02/04/2015. */ /* The scrolling logic for a TaskStackView */ public class DeckViewScroller { public interface DeckViewScrollerCallbacks { public void onScrollChanged(float p); } DeckViewConfig mConfig; DeckViewLayoutAlgorithm mLayoutAlgorithm; DeckViewScrollerCallbacks mCb; float mStackScrollP; OverScroller mScroller; ObjectAnimator mScrollAnimator; float mFinalAnimatedScroll; public DeckViewScroller(Context context, DeckViewConfig config, DeckViewLayoutAlgorithm layoutAlgorithm) { mConfig = config; mScroller = new OverScroller(context); mLayoutAlgorithm = layoutAlgorithm; setStackScroll(getStackScroll()); } /** * Resets the task scroller. */ public void reset() { mStackScrollP = 0f; } /** * Sets the callbacks */ public void setCallbacks(DeckViewScrollerCallbacks cb) { mCb = cb; } /** * Gets the current stack scroll */ public float getStackScroll() { return mStackScrollP; } /** * Sets the current stack scroll */ public void setStackScroll(float s) { mStackScrollP = s; if (mCb != null) { mCb.onScrollChanged(mStackScrollP); } } /** * Sets the current stack scroll without calling the callback. */ void setStackScrollRaw(float s) { mStackScrollP = s; } /** * Sets the current stack scroll to the initial state when you first enter recents. * * @return whether the stack progress changed. */ public boolean setStackScrollToInitialState() { float prevStackScrollP = mStackScrollP; setStackScroll(getBoundedStackScroll(mLayoutAlgorithm.mInitialScrollP)); return Float.compare(prevStackScrollP, mStackScrollP) != 0; } /** * Bounds the current scroll if necessary */ public boolean boundScroll() { float curScroll = getStackScroll(); float newScroll = getBoundedStackScroll(curScroll); if (Float.compare(newScroll, curScroll) != 0) { setStackScroll(newScroll); return true; } return false; } /** * Bounds the current scroll if necessary, but does not synchronize the stack view with the model. */ public boolean boundScrollRaw() { float curScroll = getStackScroll(); float newScroll = getBoundedStackScroll(curScroll); if (Float.compare(newScroll, curScroll) != 0) { setStackScrollRaw(newScroll); return true; } return false; } /** * Returns the bounded stack scroll */ float getBoundedStackScroll(float scroll) { return Math.max(mLayoutAlgorithm.mMinScrollP, Math.min(mLayoutAlgorithm.mMaxScrollP, scroll)); } /** * Returns the amount that the aboslute value of how much the scroll is out of bounds. */ float getScrollAmountOutOfBounds(float scroll) { if (scroll < mLayoutAlgorithm.mMinScrollP) { return Math.abs(scroll - mLayoutAlgorithm.mMinScrollP); } else if (scroll > mLayoutAlgorithm.mMaxScrollP) { return Math.abs(scroll - mLayoutAlgorithm.mMaxScrollP); } return 0f; } /** * Returns whether the specified scroll is out of bounds */ boolean isScrollOutOfBounds() { return Float.compare(getScrollAmountOutOfBounds(mStackScrollP), 0f) != 0; } /** * Animates the stack scroll into bounds */ ObjectAnimator animateBoundScroll() { float curScroll = getStackScroll(); float newScroll = getBoundedStackScroll(curScroll); if (Float.compare(newScroll, curScroll) != 0) { // Start a new scroll animation animateScroll(curScroll, newScroll, null); } return mScrollAnimator; } /** * Animates the stack scroll */ void animateScroll(float curScroll, float newScroll, final Runnable postRunnable) { // Finish any current scrolling animations if (mScrollAnimator != null && mScrollAnimator.isRunning()) { setStackScroll(mFinalAnimatedScroll); mScroller.startScroll(0, progressToScrollRange(mFinalAnimatedScroll), 0, 0, 0); } stopScroller(); stopBoundScrollAnimation(); mFinalAnimatedScroll = newScroll; mScrollAnimator = ObjectAnimator.ofFloat(this, "stackScroll", curScroll, newScroll); mScrollAnimator.setDuration(mConfig.taskStackScrollDuration); mScrollAnimator.setInterpolator(mConfig.linearOutSlowInInterpolator); mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { setStackScroll((Float) animation.getAnimatedValue()); } }); mScrollAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (postRunnable != null) { postRunnable.run(); } mScrollAnimator.removeAllListeners(); } }); mScrollAnimator.start(); } /** * Aborts any current stack scrolls */ void stopBoundScrollAnimation() { DVUtils.cancelAnimationWithoutCallbacks(mScrollAnimator); } /** * * OverScroller *** */ int progressToScrollRange(float p) { return (int) (p * mLayoutAlgorithm.mStackVisibleRect.height()); } float scrollRangeToProgress(int s) { return (float) s / mLayoutAlgorithm.mStackVisibleRect.height(); } /** * Called from the view draw, computes the next scroll. */ boolean computeScroll() { if (mScroller.computeScrollOffset()) { float scroll = scrollRangeToProgress(mScroller.getCurrY()); setStackScrollRaw(scroll); if (mCb != null) { mCb.onScrollChanged(scroll); } return true; } return false; } /** * Returns whether the overscroller is scrolling. */ boolean isScrolling() { return !mScroller.isFinished(); } /** * Stops the scroller and any current fling. */ void stopScroller() { if (!mScroller.isFinished()) { mScroller.abortAnimation(); } } } ================================================ FILE: deckview/src/main/java/com/appeaser/deckview/views/DeckViewTouchHandler.java ================================================ package com.appeaser.deckview.views; import android.content.Context; import android.view.InputDevice; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewParent; import com.appeaser.deckview.helpers.DeckViewConfig; import com.appeaser.deckview.helpers.DeckViewSwipeHelper; import com.appeaser.deckview.utilities.DVConstants; /** * Created by Vikram on 02/04/2015. */ /* Handles touch events for a TaskStackView. */ public class DeckViewTouchHandler implements DeckViewSwipeHelper.Callback { static int INACTIVE_POINTER_ID = -1; DeckViewConfig mConfig; DeckView mDeckView; DeckViewScroller mScroller; VelocityTracker mVelocityTracker; boolean mIsScrolling; float mInitialP; float mLastP; float mTotalPMotion; int mInitialMotionX, mInitialMotionY; int mLastMotionX, mLastMotionY; int mActivePointerId = INACTIVE_POINTER_ID; DeckChildView mActiveDeckChildView = null; int mMinimumVelocity; int mMaximumVelocity; // The scroll touch slop is used to calculate when we start scrolling int mScrollTouchSlop; // The page touch slop is used to calculate when we start swiping float mPagingTouchSlop; DeckViewSwipeHelper mSwipeHelper; boolean mInterceptedBySwipeHelper; public DeckViewTouchHandler(Context context, DeckView dv, DeckViewConfig config, DeckViewScroller scroller) { ViewConfiguration configuration = ViewConfiguration.get(context); mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); mScrollTouchSlop = configuration.getScaledTouchSlop(); mPagingTouchSlop = configuration.getScaledPagingTouchSlop(); mDeckView = dv; mScroller = scroller; mConfig = config; float densityScale = context.getResources().getDisplayMetrics().density; mSwipeHelper = new DeckViewSwipeHelper(DeckViewSwipeHelper.X, this, densityScale, mPagingTouchSlop); mSwipeHelper.setMinAlpha(1f); } /** * Velocity tracker helpers */ void initOrResetVelocityTracker() { if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } else { mVelocityTracker.clear(); } } void initVelocityTrackerIfNotExists() { if (mVelocityTracker == null) { mVelocityTracker = VelocityTracker.obtain(); } } void recycleVelocityTracker() { if (mVelocityTracker != null) { mVelocityTracker.recycle(); mVelocityTracker = null; } } /** * Returns the view at the specified coordinates */ DeckChildView findViewAtPoint(int x, int y) { int childCount = mDeckView.getChildCount(); for (int i = childCount - 1; i >= 0; i--) { DeckChildView tv = (DeckChildView) mDeckView.getChildAt(i); if (tv.getVisibility() == View.VISIBLE) { if (mDeckView.isTransformedTouchPointInView(x, y, tv)) { return tv; } } } return null; } /** * Constructs a simulated motion event for the current stack scroll. */ MotionEvent createMotionEventForStackScroll(MotionEvent ev) { MotionEvent pev = MotionEvent.obtainNoHistory(ev); pev.setLocation(0, mScroller.progressToScrollRange(mScroller.getStackScroll())); return pev; } /** * Touch preprocessing for handling below */ public boolean onInterceptTouchEvent(MotionEvent ev) { // Return early if we have no children boolean hasChildren = (mDeckView.getChildCount() > 0); if (!hasChildren) { return false; } // Pass through to swipe helper if we are swiping mInterceptedBySwipeHelper = mSwipeHelper.onInterceptTouchEvent(ev); if (mInterceptedBySwipeHelper) { return true; } boolean wasScrolling = mScroller.isScrolling() || (mScroller.mScrollAnimator != null && mScroller.mScrollAnimator.isRunning()); int action = ev.getAction(); switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { // Save the touch down info mInitialMotionX = mLastMotionX = (int) ev.getX(); mInitialMotionY = mLastMotionY = (int) ev.getY(); mInitialP = mLastP = mDeckView.getStackAlgorithm().screenYToCurveProgress(mLastMotionY); mActivePointerId = ev.getPointerId(0); mActiveDeckChildView = findViewAtPoint(mLastMotionX, mLastMotionY); // Stop the current scroll if it is still flinging mScroller.stopScroller(); mScroller.stopBoundScrollAnimation(); // Initialize the velocity tracker initOrResetVelocityTracker(); mVelocityTracker.addMovement(createMotionEventForStackScroll(ev)); break; } case MotionEvent.ACTION_MOVE: { if (mActivePointerId == INACTIVE_POINTER_ID) break; // Initialize the velocity tracker if necessary initVelocityTrackerIfNotExists(); mVelocityTracker.addMovement(createMotionEventForStackScroll(ev)); int activePointerIndex = ev.findPointerIndex(mActivePointerId); int y = (int) ev.getY(activePointerIndex); int x = (int) ev.getX(activePointerIndex); if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) { // Save the touch move info mIsScrolling = true; // Disallow parents from intercepting touch events final ViewParent parent = mDeckView.getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } mLastMotionX = x; mLastMotionY = y; mLastP = mDeckView.getStackAlgorithm().screenYToCurveProgress(mLastMotionY); break; } case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: { // Animate the scroll back if we've cancelled mScroller.animateBoundScroll(); // Reset the drag state and the velocity tracker mIsScrolling = false; mActivePointerId = INACTIVE_POINTER_ID; mActiveDeckChildView = null; mTotalPMotion = 0; recycleVelocityTracker(); break; } } return wasScrolling || mIsScrolling; } /** * Handles touch events once we have intercepted them */ public boolean onTouchEvent(MotionEvent ev) { // Short circuit if we have no children boolean hasChildren = (mDeckView.getChildCount() > 0); if (!hasChildren) { return false; } // Pass through to swipe helper if we are swiping if (mInterceptedBySwipeHelper && mSwipeHelper.onTouchEvent(ev)) { return true; } // Update the velocity tracker initVelocityTrackerIfNotExists(); int action = ev.getAction(); switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: { // Save the touch down info mInitialMotionX = mLastMotionX = (int) ev.getX(); mInitialMotionY = mLastMotionY = (int) ev.getY(); mInitialP = mLastP = mDeckView.getStackAlgorithm().screenYToCurveProgress(mLastMotionY); mActivePointerId = ev.getPointerId(0); mActiveDeckChildView = findViewAtPoint(mLastMotionX, mLastMotionY); // Stop the current scroll if it is still flinging mScroller.stopScroller(); mScroller.stopBoundScrollAnimation(); // Initialize the velocity tracker initOrResetVelocityTracker(); mVelocityTracker.addMovement(createMotionEventForStackScroll(ev)); // Disallow parents from intercepting touch events final ViewParent parent = mDeckView.getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } break; } case MotionEvent.ACTION_POINTER_DOWN: { final int index = ev.getActionIndex(); mActivePointerId = ev.getPointerId(index); mLastMotionX = (int) ev.getX(index); mLastMotionY = (int) ev.getY(index); mLastP = mDeckView.getStackAlgorithm().screenYToCurveProgress(mLastMotionY); break; } case MotionEvent.ACTION_MOVE: { if (mActivePointerId == INACTIVE_POINTER_ID) break; mVelocityTracker.addMovement(createMotionEventForStackScroll(ev)); int activePointerIndex = ev.findPointerIndex(mActivePointerId); int x = (int) ev.getX(activePointerIndex); int y = (int) ev.getY(activePointerIndex); int yTotal = Math.abs(y - mInitialMotionY); float curP = mDeckView.getStackAlgorithm().screenYToCurveProgress(y); float deltaP = mLastP - curP; if (!mIsScrolling) { if (yTotal > mScrollTouchSlop) { mIsScrolling = true; // Disallow parents from intercepting touch events final ViewParent parent = mDeckView.getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } } if (mIsScrolling) { float curStackScroll = mScroller.getStackScroll(); float overScrollAmount = mScroller.getScrollAmountOutOfBounds(curStackScroll + deltaP); if (Float.compare(overScrollAmount, 0f) != 0) { // Bound the overscroll to a fixed amount, and inversely scale the y-movement // relative to how close we are to the max overscroll float maxOverScroll = mConfig.taskStackOverscrollPct; deltaP *= (1f - (Math.min(maxOverScroll, overScrollAmount) / maxOverScroll)); } mScroller.setStackScroll(curStackScroll + deltaP); } mLastMotionX = x; mLastMotionY = y; mLastP = mDeckView.getStackAlgorithm().screenYToCurveProgress(mLastMotionY); mTotalPMotion += Math.abs(deltaP); break; } case MotionEvent.ACTION_UP: { mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int velocity = (int) mVelocityTracker.getYVelocity(mActivePointerId); if (mIsScrolling && (Math.abs(velocity) > mMinimumVelocity)) { float overscrollRangePct = Math.abs((float) velocity / mMaximumVelocity); int overscrollRange = (int) (Math.min(1f, overscrollRangePct) * (DVConstants.Values.DView.TaskStackMaxOverscrollRange - DVConstants.Values.DView.TaskStackMinOverscrollRange)); mScroller.mScroller.fling(0, mScroller.progressToScrollRange(mScroller.getStackScroll()), 0, velocity, 0, 0, mScroller.progressToScrollRange(mDeckView.getStackAlgorithm().mMinScrollP), mScroller.progressToScrollRange(mDeckView.getStackAlgorithm().mMaxScrollP), 0, DVConstants.Values.DView.TaskStackMinOverscrollRange + overscrollRange); // Invalidate to kick off computeScroll mDeckView.invalidate(); } else if (mScroller.isScrollOutOfBounds()) { // Animate the scroll back into bounds mScroller.animateBoundScroll(); } mActivePointerId = INACTIVE_POINTER_ID; mIsScrolling = false; mTotalPMotion = 0; recycleVelocityTracker(); break; } case MotionEvent.ACTION_POINTER_UP: { int pointerIndex = ev.getActionIndex(); int pointerId = ev.getPointerId(pointerIndex); if (pointerId == mActivePointerId) { // Select a new active pointer id and reset the motion state final int newPointerIndex = (pointerIndex == 0) ? 1 : 0; mActivePointerId = ev.getPointerId(newPointerIndex); mLastMotionX = (int) ev.getX(newPointerIndex); mLastMotionY = (int) ev.getY(newPointerIndex); mLastP = mDeckView.getStackAlgorithm().screenYToCurveProgress(mLastMotionY); mVelocityTracker.clear(); } break; } case MotionEvent.ACTION_CANCEL: { if (mScroller.isScrollOutOfBounds()) { // Animate the scroll back into bounds mScroller.animateBoundScroll(); } mActivePointerId = INACTIVE_POINTER_ID; mIsScrolling = false; mTotalPMotion = 0; recycleVelocityTracker(); break; } } return true; } /** * Handles generic motion events */ public boolean onGenericMotionEvent(MotionEvent ev) { if ((ev.getSource() & InputDevice.SOURCE_CLASS_POINTER) == InputDevice.SOURCE_CLASS_POINTER) { int action = ev.getAction(); switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_SCROLL: // Find the front most task and scroll the next task to the front float vScroll = ev.getAxisValue(MotionEvent.AXIS_VSCROLL); if (vScroll > 0) { if (mDeckView.ensureFocusedTask()) { mDeckView.focusNextTask(true, false); } } else { if (mDeckView.ensureFocusedTask()) { mDeckView.focusNextTask(false, false); } } return true; } } return false; } /** * * SwipeHelper Implementation *** */ @Override public View getChildAtPosition(MotionEvent ev) { return findViewAtPoint((int) ev.getX(), (int) ev.getY()); } @Override public boolean canChildBeDismissed(View v) { return true; } @Override public void onBeginDrag(View v) { DeckChildView tv = (DeckChildView) v; // Disable clipping with the stack while we are swiping tv.setClipViewInStack(false); // Disallow touch events from this task view tv.setTouchEnabled(false); // Disallow parents from intercepting touch events final ViewParent parent = mDeckView.getParent(); if (parent != null) { parent.requestDisallowInterceptTouchEvent(true); } } @Override public void onSwipeChanged(View v, float delta) { // Do nothing } @Override public void onChildDismissed(View v) { DeckChildView tv = (DeckChildView) v; // Re-enable clipping with the stack (we will reuse this view) tv.setClipViewInStack(true); // Re-enable touch events from this task view tv.setTouchEnabled(true); // Remove the task view from the stack mDeckView.onDeckChildViewDismissed(tv); } @Override public void onSnapBackCompleted(View v) { DeckChildView tv = (DeckChildView) v; // Re-enable clipping with the stack tv.setClipViewInStack(true); // Re-enable touch events from this task view tv.setTouchEnabled(true); } @Override public void onDragCancelled(View v) { // Do nothing } } ================================================ FILE: deckview/src/main/java/com/appeaser/deckview/views/FixedSizeImageView.java ================================================ package com.appeaser.deckview.views; /** * Created by Vikram on 01/04/2015. */ import android.content.Context; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.widget.ImageView; /** * This is an optimized ImageView that does not trigger a requestLayout() or invalidate() when * setting the image to Null. */ public class FixedSizeImageView extends ImageView { boolean mAllowRelayout = true; boolean mAllowInvalidate = true; public FixedSizeImageView(Context context) { this(context, null); } public FixedSizeImageView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public FixedSizeImageView(Context context, AttributeSet attrs, int defStyleAttr) { this(context, attrs, defStyleAttr, 0); } public FixedSizeImageView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @Override public void requestLayout() { if (mAllowRelayout) { super.requestLayout(); } } @Override public void invalidate() { if (mAllowInvalidate) { super.invalidate(); } } @Override public void setImageDrawable(Drawable drawable) { boolean isNullBitmapDrawable = (drawable instanceof BitmapDrawable) && (((BitmapDrawable) drawable).getBitmap() == null); if (drawable == null || isNullBitmapDrawable) { mAllowRelayout = false; mAllowInvalidate = false; } super.setImageDrawable(drawable); mAllowRelayout = true; mAllowInvalidate = true; } } ================================================ FILE: deckview/src/main/java/com/appeaser/deckview/views/ViewAnimation.java ================================================ package com.appeaser.deckview.views; import android.animation.ValueAnimator; import android.graphics.Rect; import com.appeaser.deckview.helpers.DeckChildViewTransform; import com.appeaser.deckview.utilities.ReferenceCountedTrigger; /** * Created by Vikram on 01/04/2015. */ /* Common code related to view animations */ public class ViewAnimation { /* The animation context for a task view animation into Recents */ public static class TaskViewEnterContext { // A trigger to run some logic when all the animations complete. This works around the fact // that it is difficult to coordinate ViewPropertyAnimators public ReferenceCountedTrigger postAnimationTrigger; // An update listener to notify as the enter animation progresses (used for the home transition) public ValueAnimator.AnimatorUpdateListener updateListener; // These following properties are updated for each task view we start the enter animation on // Whether or not the current task occludes the launch target boolean currentTaskOccludesLaunchTarget; // The task rect for the current stack Rect currentTaskRect; // The transform of the current task view public DeckChildViewTransform currentTaskTransform; // The view index of the current task view public int currentStackViewIndex; // The total number of task views public int currentStackViewCount; public TaskViewEnterContext(ReferenceCountedTrigger t) { postAnimationTrigger = t; } } /* The animation context for a task view animation out of Recents */ public static class TaskViewExitContext { // A trigger to run some logic when all the animations complete. This works around the fact // that it is difficult to coordinate ViewPropertyAnimators public ReferenceCountedTrigger postAnimationTrigger; // The translationY to apply to a TaskView to move it off the bottom of the task stack public int offscreenTranslationY; public TaskViewExitContext(ReferenceCountedTrigger t) { postAnimationTrigger = t; } } } ================================================ FILE: deckview/src/main/java/com/appeaser/deckview/views/ViewPool.java ================================================ package com.appeaser.deckview.views; import android.content.Context; import java.util.Iterator; import java.util.LinkedList; /** * Created by Vikram on 01/04/2015. */ /* A view pool to manage more views than we can visibly handle */ public class ViewPool { /* An interface to the consumer of a view pool */ public interface ViewPoolConsumer { public V createView(Context context); public void prepareViewToEnterPool(V v); public void prepareViewToLeavePool(V v, T prepareData, boolean isNewView); public boolean hasPreferredData(V v, T preferredData); } Context mContext; ViewPoolConsumer mViewCreator; LinkedList mPool = new LinkedList(); /** * Initializes the pool with a fixed predetermined pool size */ public ViewPool(Context context, ViewPoolConsumer viewCreator) { mContext = context; mViewCreator = viewCreator; } /** * Returns a view into the pool */ void returnViewToPool(V v) { mViewCreator.prepareViewToEnterPool(v); mPool.push(v); } /** * Gets a view from the pool and prepares it */ V pickUpViewFromPool(T preferredData, T prepareData) { V v = null; boolean isNewView = false; if (mPool.isEmpty()) { v = mViewCreator.createView(mContext); isNewView = true; } else { // Try and find a preferred view Iterator iter = mPool.iterator(); while (iter.hasNext()) { V vpv = iter.next(); if (mViewCreator.hasPreferredData(vpv, preferredData)) { v = vpv; iter.remove(); break; } } // Otherwise, just grab the first view if (v == null) { v = mPool.pop(); } } mViewCreator.prepareViewToLeavePool(v, prepareData, isNewView); return v; } /** * Returns an iterator to the list of the views in the pool. */ Iterator poolViewIterator() { if (mPool != null) { return mPool.iterator(); } return null; } } ================================================ FILE: deckview/src/main/res/drawable-v21/deck_child_view_button_bg.xml ================================================ ================================================ FILE: deckview/src/main/res/drawable-v21/deck_child_view_dismiss_dark.xml ================================================ ================================================ FILE: deckview/src/main/res/drawable-v21/deck_child_view_dismiss_light.xml ================================================ ================================================ FILE: deckview/src/main/res/drawable-v21/deck_child_view_header_bg.xml ================================================ ================================================ FILE: deckview/src/main/res/drawable-v21/deck_child_view_header_bg_color.xml ================================================ ================================================ FILE: deckview/src/main/res/interpolator-v21/decelerate_quint.xml ================================================ ================================================ FILE: deckview/src/main/res/interpolator-v21/fast_out_linear_in.xml ================================================ ================================================ FILE: deckview/src/main/res/interpolator-v21/fast_out_slow_in.xml ================================================ ================================================ FILE: deckview/src/main/res/interpolator-v21/linear_out_slow_in.xml ================================================ ================================================ FILE: deckview/src/main/res/layout/deck_child_view.xml ================================================ ================================================ FILE: deckview/src/main/res/layout/deck_child_view_header.xml ================================================ ================================================ FILE: deckview/src/main/res/values/colors.xml ================================================ #ffe6e6e6 #ffeeeeee #cc000000 #28ffffff #44000000 #03000000 ================================================ FILE: deckview/src/main/res/values/config.xml ================================================ 250 250 225 96 325 100 200 225 12 125 225 250 1 400 false 200 false 212 0 ================================================ FILE: deckview/src/main/res/values/dimens.xml ================================================ 800dp 0.03333 0.0875 16dp 100dp 2dp 1.5dp 20dp 80dp 64dp 0.6 56dp 0.9 1dp 8dp 48dp ================================================ FILE: deckview/src/main/res/values/strings.xml ================================================ DeckViewLibrary Nothing here Dismiss %s. ================================================ FILE: deckview/src/main/res/values-land/dimens.xml ================================================ 0.26 ================================================ FILE: deckview/src/main/res/values-sw600dp/dimens.xml ================================================ 0.075 ================================================ FILE: deckview/src/main/res/values-sw600dp-land/dimens.xml ================================================ 0.25 ================================================ FILE: deckview/src/main/res/values-sw720dp/dimens.xml ================================================ 3dp ================================================ FILE: deckviewsample/.gitignore ================================================ /build ================================================ FILE: deckviewsample/build.gradle ================================================ apply plugin: 'com.android.application' android { compileSdkVersion 22 buildToolsVersion "22.0.1" defaultConfig { applicationId "com.appeaser.deckviewsample" minSdkVersion 21 targetSdkVersion 22 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.squareup.picasso:picasso:2.5.2' compile project(':deckview') } ================================================ FILE: deckviewsample/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in C:/Users/Vikram/Documents/Android/adt-bundle-windows-x86-20130219/sdk/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} ================================================ FILE: deckviewsample/src/androidTest/java/com/appeaser/deckviewsample/ApplicationTest.java ================================================ package com.appeaser.deckviewsample; import android.app.Application; import android.test.ApplicationTestCase; /** * Testing Fundamentals */ public class ApplicationTest extends ApplicationTestCase { public ApplicationTest() { super(Application.class); } } ================================================ FILE: deckviewsample/src/main/AndroidManifest.xml ================================================ ================================================ FILE: deckviewsample/src/main/java/com/appeaser/deckviewsample/Datum.java ================================================ package com.appeaser.deckviewsample; import android.os.Parcel; import android.os.Parcelable; import com.squareup.picasso.Target; /** * Simple model class * One important requirement for DeckView to function * is that all items in the dataset *must be* uniquely * identifiable. No two items can be such * that `item1.equals(item2)` returns `true`. * See equals() implementation below. * `id` is generated using `DeckViewSampleActivity#generateuniqueKey()` * Implementing `Parcelable` serves only one purpose - to persist data * on configuration change. */ public class Datum implements Parcelable { public int id; public String headerTitle, link; public Target target; public Datum() { // Nothing } @Override public int describeContents() { return 0; } public Datum(Parcel in) { readFromParcel(in); } public void readFromParcel(Parcel in) { id = in.readInt(); headerTitle = in.readString(); link = in.readString(); } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(id); dest.writeString(headerTitle); dest.writeString(link); } public static final Creator CREATOR = new Creator() { public Datum createFromParcel(Parcel in) { return new Datum(in); } public Datum[] newArray(int size) { return new Datum[size]; } }; @Override public boolean equals(Object o) { return ((Datum) o).id == this.id; } } ================================================ FILE: deckviewsample/src/main/java/com/appeaser/deckviewsample/DeckViewSampleActivity.java ================================================ package com.appeaser.deckviewsample; import android.app.Activity; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Color; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.widget.Toast; import com.appeaser.deckview.views.DeckChildView; import com.appeaser.deckview.views.DeckView; import com.squareup.picasso.Picasso; import com.squareup.picasso.Target; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Random; /** * Basic sample for DeckView. * Images are downloaded and cached using * Picasso "http://square.github.io/picasso/". * DeckView is *very* young & can only * afford basic functionality. */ public class DeckViewSampleActivity extends Activity { // View that stacks its children like a deck of cards DeckView mDeckView; Drawable mDefaultHeaderIcon; ArrayList mEntries; // Placeholder for when the image is being downloaded Bitmap mDefaultThumbnail; // Retain position on configuration change // imageSize to pass to http://lorempixel.com int scrollToChildIndex = -1, imageSize = 500; // SavedInstance bundle keys final String CURRENT_SCROLL = "current.scroll", CURRENT_LIST = "current.list"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_deck_view_sample); mDeckView = (DeckView) findViewById(R.id.deckview); mDefaultThumbnail = BitmapFactory.decodeResource(getResources(), R.drawable.default_thumbnail); mDefaultHeaderIcon = getResources().getDrawable(R.drawable.default_header_icon); if (savedInstanceState != null) { if (savedInstanceState.containsKey(CURRENT_LIST)) { mEntries = savedInstanceState.getParcelableArrayList(CURRENT_LIST); } if (savedInstanceState.containsKey(CURRENT_SCROLL)) { scrollToChildIndex = savedInstanceState.getInt(CURRENT_SCROLL); } } if (mEntries == null) { mEntries = new ArrayList<>(); for (int i = 1; i < 100; i++) { Datum datum = new Datum(); datum.id = generateUniqueKey(); datum.link = "http://lorempixel.com/" + imageSize + "/" + imageSize + "/sports/" + "ID " + datum.id + "/"; datum.headerTitle = "Image ID " + datum.id; mEntries.add(datum); } } // Callback implementation DeckView.Callback deckViewCallback = new DeckView.Callback() { @Override public ArrayList getData() { return mEntries; } @Override public void loadViewData(WeakReference> dcv, Datum item) { loadViewDataInternal(item, dcv); } @Override public void unloadViewData(Datum item) { Picasso.with(DeckViewSampleActivity.this).cancelRequest(item.target); } @Override public void onViewDismissed(Datum item) { mEntries.remove(item); mDeckView.notifyDataSetChanged(); } @Override public void onItemClick(Datum item) { Toast.makeText(DeckViewSampleActivity.this, "Item with title: '" + item.headerTitle + "' clicked", Toast.LENGTH_SHORT).show(); } @Override public void onNoViewsToDeck() { Toast.makeText(DeckViewSampleActivity.this, "No views to show", Toast.LENGTH_SHORT).show(); } }; mDeckView.initialize(deckViewCallback); if (scrollToChildIndex != -1) { mDeckView.post(new Runnable() { @Override public void run() { // Restore scroll position mDeckView.scrollToChild(scrollToChildIndex); } }); } } void loadViewDataInternal(final Datum datum, final WeakReference> weakView) { // datum.target can be null Picasso.with(DeckViewSampleActivity.this).cancelRequest(datum.target); datum.target = new Target() { @Override public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) { // Pass loaded Bitmap to view if (weakView.get() != null) { weakView.get().onDataLoaded(datum, bitmap, mDefaultHeaderIcon, datum.headerTitle, Color.DKGRAY); } } @Override public void onBitmapFailed(Drawable errorDrawable) { // Loading failed. Pass default thumbnail instead if (weakView.get() != null) { weakView.get().onDataLoaded(datum, mDefaultThumbnail, mDefaultHeaderIcon, datum.headerTitle + " Failed", Color.DKGRAY); } } @Override public void onPrepareLoad(Drawable placeHolderDrawable) { // Pass the default thumbnail for now. It will // be replaced once the target Bitmap has been loaded if (weakView.get() != null) { weakView.get().onDataLoaded(datum, mDefaultThumbnail, mDefaultHeaderIcon, "Loading...", Color.DKGRAY); } } }; // Begin loading Picasso.with(DeckViewSampleActivity.this).load(datum.link).into(datum.target); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_deck_view_sample, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); // Add a new item to the end of the list if (id == R.id.action_add) { Datum datum = new Datum(); datum.id = generateUniqueKey(); datum.headerTitle = "(New) Image ID " + datum.id; datum.link = "http://lorempixel.com/" + imageSize + "/" + imageSize + "/sports/" + "ID " + datum.id + "/"; mEntries.add(datum); mDeckView.notifyDataSetChanged(); return true; } else if (id == R.id.action_add_multiple) { // Add multiple items (between 5 & 10 items) // at random indices Random rand = new Random(); // adding between 5 and 10 items int numberOfItemsToAdd = rand.nextInt(6) + 5; for (int i = 0; i < numberOfItemsToAdd; i++) { int atIndex = mEntries.size() > 0 ? rand.nextInt(mEntries.size()) : 0; Datum datum = new Datum(); datum.id = generateUniqueKey(); datum.link = "http://lorempixel.com/" + imageSize + "/" + imageSize + "/sports/" + "ID " + datum.id + "/"; datum.headerTitle = "(New) Image ID " + datum.id; mEntries.add(atIndex, datum); } mDeckView.notifyDataSetChanged(); return true; } return super.onOptionsItemSelected(item); } @Override protected void onSaveInstanceState(Bundle outState) { // Save current scroll and the list int currentChildIndex = mDeckView.getCurrentChildIndex(); outState.putInt(CURRENT_SCROLL, currentChildIndex); outState.putParcelableArrayList(CURRENT_LIST, mEntries); super.onSaveInstanceState(outState); } // Generates a key that will remain unique // during the application's lifecycle private static int generateUniqueKey() { return ++KEY; } private static int KEY = 0; } ================================================ FILE: deckviewsample/src/main/res/drawable-v21/box.xml ================================================ ================================================ FILE: deckviewsample/src/main/res/layout/activity_deck_view_sample.xml ================================================ ================================================ FILE: deckviewsample/src/main/res/menu/menu_deck_view_sample.xml ================================================ ================================================ FILE: deckviewsample/src/main/res/values/dimens.xml ================================================ 16dp 16dp ================================================ FILE: deckviewsample/src/main/res/values/strings.xml ================================================ DeckViewSample Hello world! Settings ================================================ FILE: deckviewsample/src/main/res/values/styles.xml ================================================ ================================================ FILE: deckviewsample/src/main/res/values-v21/styles.xml ================================================ ================================================ FILE: deckviewsample/src/main/res/values-w820dp/dimens.xml ================================================ 64dp ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Wed Apr 10 15:27:10 PDT 2013 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip ================================================ FILE: gradle.properties ================================================ # Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. # For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. # Default value: -Xmx10248m -XX:MaxPermSize=256m # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true ================================================ FILE: gradlew ================================================ #!/usr/bin/env bash ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn ( ) { echo "$*" } die ( ) { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; esac # For Cygwin, ensure paths are in UNIX format before anything is touched. if $cygwin ; then [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` fi # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >&- APP_HOME="`pwd -P`" cd "$SAVED" >&- CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules function splitJvmOpts() { JVM_OPTS=("$@") } eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windowz variants if not "%OS%" == "Windows_NT" goto win9xME_args if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* goto execute :4NT_args @rem Get arguments from the 4NT Shell from JP Software set CMD_LINE_ARGS=%$ :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: settings.gradle ================================================ include ':app', ':deckview', ':deckviewsample'