diff --git a/expanding-collection/.gitignore b/expanding-collection/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/expanding-collection/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/expanding-collection/build.gradle b/expanding-collection/build.gradle
new file mode 100644
index 0000000..a756f12
--- /dev/null
+++ b/expanding-collection/build.gradle
@@ -0,0 +1,128 @@
+apply plugin: 'com.android.library'
+apply plugin: 'signing'
+apply plugin: 'com.bmuschko.nexus'
+apply plugin: "jacoco"
+
+group = 'ml.geekypanda'
+version = '1.0.0'
+
+android {
+ compileSdkVersion 29
+ defaultConfig {
+ minSdkVersion 14
+ targetSdkVersion 29
+ versionCode 3
+ versionName version
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ debug {
+ versionNameSuffix "Debug"
+ debuggable true
+ testCoverageEnabled = true
+ }
+ }
+ compileOptions {
+ targetCompatibility JavaVersion.VERSION_1_8
+ sourceCompatibility JavaVersion.VERSION_1_8
+ }
+ testOptions {
+ unitTests.returnDefaultValues = true
+ }
+ productFlavors {
+ }
+}
+
+dependencies {
+ implementation fileTree(include: ['*.jar'], dir: 'libs')
+ implementation 'androidx.appcompat:appcompat:1.0.2'
+ androidTestImplementation "androidx.test.espresso:espresso-core:3.1.1"
+ implementation "com.github.bumptech.glide:glide:$rootProject.glideVersion"
+ annotationProcessor "com.github.bumptech.glide:compiler:$rootProject.glideVersion"
+ testImplementation 'junit:junit:4.12'
+}
+
+modifyPom {
+ project {
+ name 'Expanding Collection for Android'
+ description 'ExpandingCollection is a card peek/pop controller https://geekypanda.ml'
+ url 'https://github.com/upendra-bajpai/expanding-collection-network'
+ inceptionYear '2020'
+
+ scm {
+ url 'https://github.com/upendra-bajpai/expanding-collection-network'
+ connection 'scm:git@github.com:upendra-bajpai/expanding-collection-network.git'
+ developerConnection 'scm:git@github.com:upendra-bajpai/expanding-collection-network.git'
+ }
+
+ licenses {
+ license {
+ name 'The MIT License (MIT)'
+ url 'https://opensource.org/licenses/mit-license.php'
+ distribution 'repo'
+ }
+ }
+
+ developers {
+ developer {
+ id 'upendra-bajpai'
+ name 'Upendra Bajpai'
+ email 'supernovaplazma@gmail.com'
+ }
+
+ }
+ }
+}
+
+extraArchive {
+ sources = false
+ tests = false
+ javadoc = false
+}
+
+nexus {
+ sign = true
+ repositoryUrl = 'https://oss.sonatype.org/service/local/staging/deploy/maven2/'
+ snapshotRepositoryUrl = 'https://oss.sonatype.org/content/repositories/snapshots/'
+}
+
+jacoco {
+ toolVersion = "0.7.6.201602180812"
+ reportsDir = file("$buildDir/reports/jacoco")
+
+}
+
+task jacocoReport(type: JacocoReport, dependsOn: 'testDebugUnitTest') {
+ group = 'Reporting'
+ description = 'Generate Jacoco coverage reports after running tests.'
+
+ reports {
+ xml.enabled = true
+ html.enabled = true
+ }
+
+ classDirectories = fileTree(
+ dir: 'build/intermediates/classes/debug',
+ excludes: [
+ '**/R*.class',
+ '**/BuildConfig*'
+ ]
+ )
+
+ sourceDirectories = files('src/main/java')
+ executionData = files('build/jacoco/testDebugUnitTest.exec')
+
+ doFirst {
+ files('build/intermediates/classes/debug').getFiles().each { file ->
+ if (file.name.contains('$$')) {
+ file.renameTo(file.path.replace('$$', '$'))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/expanding-collection/expanding_collection.iml b/expanding-collection/expanding_collection.iml
new file mode 100644
index 0000000..36cb70e
--- /dev/null
+++ b/expanding-collection/expanding_collection.iml
@@ -0,0 +1,141 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ generateDebugSources
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/expanding-collection/proguard-rules.pro b/expanding-collection/proguard-rules.pro
new file mode 100644
index 0000000..e5b4948
--- /dev/null
+++ b/expanding-collection/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /home/kid/Development/android-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 *;
+#}
diff --git a/expanding-collection/src/main/AndroidManifest.xml b/expanding-collection/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..9016235
--- /dev/null
+++ b/expanding-collection/src/main/AndroidManifest.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
diff --git a/expanding-collection/src/main/java/com/ramotion/expandingcollection/AlphaScalePageTransformer.java b/expanding-collection/src/main/java/com/ramotion/expandingcollection/AlphaScalePageTransformer.java
new file mode 100644
index 0000000..38d7aa1
--- /dev/null
+++ b/expanding-collection/src/main/java/com/ramotion/expandingcollection/AlphaScalePageTransformer.java
@@ -0,0 +1,32 @@
+package com.ramotion.expandingcollection;
+
+import android.view.View;
+
+import androidx.viewpager.widget.ViewPager;
+
+/**
+ * Page Transformer for main ViewPager.
+ */
+public class AlphaScalePageTransformer implements ViewPager.PageTransformer {
+
+ private static final float INACTIVE_SCALE = 0.8f;
+ private static final float INACTIVE_ALPHA = 0.5f;
+
+ public void transformPage(View view, float position) {
+ if (position < -1) {
+ view.setAlpha(INACTIVE_ALPHA);
+ view.setScaleX(INACTIVE_SCALE);
+ view.setScaleY(INACTIVE_SCALE);
+ } else if (position <= 1) {
+ float scale = INACTIVE_SCALE + (1 - INACTIVE_SCALE) * (1 - Math.abs(position));
+ float alpha = INACTIVE_ALPHA + (1 - INACTIVE_ALPHA) * (1 - Math.abs(position));
+ view.setScaleX(scale);
+ view.setScaleY(scale);
+ view.setAlpha(alpha);
+ } else {
+ view.setAlpha(INACTIVE_ALPHA);
+ view.setScaleX(INACTIVE_SCALE);
+ view.setScaleY(INACTIVE_SCALE);
+ }
+ }
+}
diff --git a/expanding-collection/src/main/java/com/ramotion/expandingcollection/BackgroundBitmapCache.java b/expanding-collection/src/main/java/com/ramotion/expandingcollection/BackgroundBitmapCache.java
new file mode 100644
index 0000000..2eef9a6
--- /dev/null
+++ b/expanding-collection/src/main/java/com/ramotion/expandingcollection/BackgroundBitmapCache.java
@@ -0,0 +1,51 @@
+package com.ramotion.expandingcollection;
+
+import android.graphics.Bitmap;
+import android.util.LruCache;
+
+/**
+ * LruCache for caching background bitmaps for {@link ECBackgroundSwitcherView}.
+ * Key is id of page from {@link ECPager} and value is background bitmap from provided data.
+ */
+public class BackgroundBitmapCache {
+ private LruCache mBackgroundsCache;
+
+ private static BackgroundBitmapCache instance;
+
+ public static BackgroundBitmapCache getInstance() {
+ if (instance == null) {
+ instance = new BackgroundBitmapCache();
+ instance.init();
+ }
+ return instance;
+ }
+
+ private void init() {
+ final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
+ final int cacheSize = maxMemory / 5;
+
+ mBackgroundsCache = new LruCache(cacheSize) {
+ @Override
+ protected void entryRemoved(boolean evicted, Integer key, Bitmap oldValue, Bitmap newValue) {
+ super.entryRemoved(evicted, key, oldValue, newValue);
+ }
+
+ @Override
+ protected int sizeOf(Integer key, Bitmap bitmap) {
+ // The cache size will be measured in kilobytes rather than number of items.
+ return bitmap.getByteCount() / 1024;
+ }
+ };
+ }
+
+ public void addBitmapToBgMemoryCache(Integer key, Bitmap bitmap) {
+ if (key!=null&&getBitmapFromBgMemCache(key) == null) {
+ mBackgroundsCache.put(key, bitmap);
+ }
+ }
+
+ public Bitmap getBitmapFromBgMemCache(Integer key) {
+ return mBackgroundsCache.get(key);
+ }
+
+}
diff --git a/expanding-collection/src/main/java/com/ramotion/expandingcollection/BitmapFactoryOptions.java b/expanding-collection/src/main/java/com/ramotion/expandingcollection/BitmapFactoryOptions.java
new file mode 100644
index 0000000..2440a04
--- /dev/null
+++ b/expanding-collection/src/main/java/com/ramotion/expandingcollection/BitmapFactoryOptions.java
@@ -0,0 +1,12 @@
+package com.ramotion.expandingcollection;
+
+import android.graphics.BitmapFactory;
+
+/**
+ * Bitmap Factory Options with inScaled flag disabled by default
+ */
+public class BitmapFactoryOptions extends BitmapFactory.Options {
+ public BitmapFactoryOptions() {
+ this.inScaled = false;
+ }
+}
diff --git a/expanding-collection/src/main/java/com/ramotion/expandingcollection/BitmapWorkerTask.java b/expanding-collection/src/main/java/com/ramotion/expandingcollection/BitmapWorkerTask.java
new file mode 100644
index 0000000..2da1f8c
--- /dev/null
+++ b/expanding-collection/src/main/java/com/ramotion/expandingcollection/BitmapWorkerTask.java
@@ -0,0 +1,37 @@
+package com.ramotion.expandingcollection;
+
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.AsyncTask;
+
+import androidx.annotation.DrawableRes;
+
+/**
+ * Worker for async processing bitmaps through cache {@link BackgroundBitmapCache}
+ */
+public class BitmapWorkerTask extends AsyncTask {
+
+ private final Resources mResources;
+ private final BackgroundBitmapCache cache;
+ private final int mProvidedBitmapResId;
+
+ public BitmapWorkerTask(Resources resources, @DrawableRes int providedBitmapResId) {
+ this.mResources = resources;
+ this.cache = BackgroundBitmapCache.getInstance();
+ this.mProvidedBitmapResId = providedBitmapResId;
+ }
+
+ @Override
+ protected Bitmap doInBackground(Integer... params) {
+ Integer key = params[0];
+ Bitmap cachedBitmap = cache.getBitmapFromBgMemCache(key);
+ if (cachedBitmap == null) {
+ cachedBitmap = BitmapFactory.decodeResource(mResources, mProvidedBitmapResId, new BitmapFactoryOptions());
+ cache.addBitmapToBgMemoryCache(mProvidedBitmapResId, cachedBitmap);
+ }
+ return cachedBitmap;
+ }
+
+
+}
diff --git a/expanding-collection/src/main/java/com/ramotion/expandingcollection/ECBackgroundSwitcherView.java b/expanding-collection/src/main/java/com/ramotion/expandingcollection/ECBackgroundSwitcherView.java
new file mode 100644
index 0000000..cfeb88f
--- /dev/null
+++ b/expanding-collection/src/main/java/com/ramotion/expandingcollection/ECBackgroundSwitcherView.java
@@ -0,0 +1,210 @@
+package com.ramotion.expandingcollection;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.AsyncTask;
+import android.os.StrictMode;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.view.View;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.TranslateAnimation;
+import android.widget.FrameLayout;
+import android.widget.ImageSwitcher;
+import android.widget.ImageView;
+import android.widget.ViewSwitcher;
+
+import com.bumptech.glide.Glide;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Random;
+
+/**
+ * Custom Image Switcher for display and change background images with some pretty animations.
+ * Uses different drawing orders for animation purposes.
+ */
+public class ECBackgroundSwitcherView extends ImageSwitcher {
+ private final int[] REVERSE_ORDER = new int[]{1, 0};
+ private final int[] NORMAL_ORDER = new int[]{0, 1};
+
+ private boolean reverseDrawOrder;
+
+ private int bgImageGap;
+ private int bgImageWidth;
+
+ private int alphaDuration = 400;
+ private int movementDuration = 500;
+ private int widthBackgroundImageGapPercent = 12;
+
+ private Animation bgImageInLeftAnimation;
+ private Animation bgImageOutLeftAnimation;
+
+ private Animation bgImageInRightAnimation;
+ private Animation bgImageOutRightAnimation;
+
+ private AnimationDirection currentAnimationDirection;
+
+ private BitmapWorkerTask mCurrentAnimationTask;
+
+ public ECBackgroundSwitcherView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ inflateAndInit(context);
+ }
+
+ public ECBackgroundSwitcherView(Context context) {
+ super(context);
+ inflateAndInit(context);
+ }
+
+ private void inflateAndInit(final Context context) {
+ setChildrenDrawingOrderEnabled(true);
+ DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
+ bgImageGap = (displayMetrics.widthPixels / 100) * widthBackgroundImageGapPercent;
+ bgImageWidth = displayMetrics.widthPixels + bgImageGap * 2;
+
+ this.setFactory(new ViewSwitcher.ViewFactory() {
+ public View makeView() {
+ ImageView myView = new ImageView(context);
+ myView.setScaleType(ImageView.ScaleType.CENTER_CROP);
+ myView.setLayoutParams(new FrameLayout.LayoutParams(bgImageWidth, FrameLayout.LayoutParams.MATCH_PARENT));
+ myView.setTranslationX(-bgImageGap);
+ return myView;
+ }
+ });
+
+ bgImageInLeftAnimation = createBgImageInAnimation(bgImageGap, 0, movementDuration, alphaDuration);
+ bgImageOutLeftAnimation = createBgImageOutAnimation(0, -bgImageGap, movementDuration);
+ bgImageInRightAnimation = createBgImageInAnimation(-bgImageGap, 0, movementDuration, alphaDuration);
+ bgImageOutRightAnimation = createBgImageOutAnimation(0, bgImageGap, movementDuration);
+ }
+
+// public ECBackgroundSwitcherView withAnimationSettings(int movementDuration, int alphaDuration) {
+// this.movementDuration = movementDuration;
+// this.alphaDuration = alphaDuration;
+// bgImageInLeftAnimation = createBgImageInAnimation(bgImageGap, 0, movementDuration, alphaDuration);
+// bgImageOutLeftAnimation = createBgImageOutAnimation(0, -bgImageGap, movementDuration);
+// bgImageInRightAnimation = createBgImageInAnimation(-bgImageGap, 0, movementDuration, alphaDuration);
+// bgImageOutRightAnimation = createBgImageOutAnimation(0, bgImageGap, movementDuration);
+// return this;
+// }
+
+ @Override
+ protected int getChildDrawingOrder(int childCount, int i) {
+ if (reverseDrawOrder)
+ return REVERSE_ORDER[i];
+ else
+ return NORMAL_ORDER[i];
+ }
+
+ public void setReverseDrawOrder(boolean reverseDrawOrder) {
+ this.reverseDrawOrder = reverseDrawOrder;
+ }
+
+ private synchronized void setImageBitmapWithAnimation(String newBitmap, AnimationDirection animationDirection) {
+ if (this.currentAnimationDirection == animationDirection) {
+ this.setImageBitmap(newBitmap);
+ } else if (animationDirection == AnimationDirection.LEFT) {
+ this.setInAnimation(bgImageInLeftAnimation);
+ this.setOutAnimation(bgImageOutLeftAnimation);
+ this.setImageBitmap(newBitmap);
+ } else if (animationDirection == AnimationDirection.RIGHT) {
+ this.setInAnimation(bgImageInRightAnimation);
+ this.setOutAnimation(bgImageOutRightAnimation);
+ this.setImageBitmap(newBitmap);
+ }
+ this.currentAnimationDirection = animationDirection;
+ }
+
+ public void cacheBackgroundAtPosition(ECPager pager, int position) {
+ if (position >= 0 && position < pager.getAdapter().getCount()) {
+ Integer mainBgImageDrawableResource = pager.getDataFromAdapterDataset(position).getKey();
+ if (mainBgImageDrawableResource == null) return;
+ BitmapWorkerTask addBitmapToCacheTask = new BitmapWorkerTask(getResources(), mainBgImageDrawableResource);
+ addBitmapToCacheTask.execute(mainBgImageDrawableResource);
+ }
+ }
+
+ public void updateCurrentBackground(ECPager pager, final AnimationDirection direction) {
+ int position = pager.getCurrentPosition();
+ BackgroundBitmapCache instance = BackgroundBitmapCache.getInstance();
+ String mainBgImageDrawableResource = pager.getDataFromAdapterDataset(position).getMainBackgroundResource();
+ Integer key=pager.getDataFromAdapterDataset(position).getKey();
+ String url=pager.getDataFromAdapterDataset(position).getMainBackgroundResource();
+ if (key!=null) {
+ Bitmap cachedBitmap = instance.getBitmapFromBgMemCache(key);
+ /*if (cachedBitmap == null) {
+ if (mainBgImageDrawableResource == null) return;
+ StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
+ StrictMode.setThreadPolicy(policy);
+ try {
+ URL url = new URL(mainBgImageDrawableResource);
+ cachedBitmap = BitmapFactory.decodeStream((InputStream) url.getContent());
+ instance.addBitmapToBgMemoryCache(key, cachedBitmap);
+ } catch (IOException e) {
+ //Log.e(TAG, e.getMessage());
+ }
+
+ }*/
+
+ setImageBitmapWithAnimation(url, direction);
+ }
+ }
+
+ public void updateCurrentBackgroundAsync(ECPager pager, final AnimationDirection direction) {
+ if (mCurrentAnimationTask != null && mCurrentAnimationTask.getStatus().equals(AsyncTask.Status.RUNNING)) {
+ getInAnimation().cancel();
+ }
+ int position = pager.getCurrentPosition();
+ Integer mainBgImageDrawableResource = pager.getDataFromAdapterDataset(position).getKey();
+ String url = pager.getDataFromAdapterDataset(position).getMainBackgroundResource();
+ if (mainBgImageDrawableResource == null) return;
+ setImageBitmapWithAnimation(url, direction);
+ /* mCurrentAnimationTask = new BitmapWorkerTask(getResources(), mainBgImageDrawableResource) {
+ @Override
+ protected void onPostExecute(Bitmap bitmap) {
+ super.onPostExecute(bitmap);
+ setImageBitmapWithAnimation(bitmap, direction);
+ }
+ };
+ mCurrentAnimationTask.execute(mainBgImageDrawableResource);*/
+ }
+
+
+ private void setImageBitmap(String bitmap) {
+ ImageView image = (ImageView) this.getNextView();
+ Glide.with(getContext()).load(bitmap).into(image);
+ //image.setImageBitmap(bitmap);
+ showNext();
+ }
+
+ private Animation createBgImageInAnimation(int fromX, int toX, int transitionDuration, int alphaDuration) {
+ TranslateAnimation translate = new TranslateAnimation(fromX, toX, 0, 0);
+ translate.setDuration(transitionDuration);
+
+ AlphaAnimation alpha = new AlphaAnimation(0F, 1F);
+ alpha.setDuration(alphaDuration);
+
+ AnimationSet set = new AnimationSet(true);
+ set.setInterpolator(new DecelerateInterpolator());
+ set.addAnimation(translate);
+ set.addAnimation(alpha);
+ return set;
+ }
+
+ private Animation createBgImageOutAnimation(int fromX, int toX, int transitionDuration) {
+ TranslateAnimation ta = new TranslateAnimation(fromX, toX, 0, 0);
+ ta.setDuration(transitionDuration);
+ ta.setInterpolator(new DecelerateInterpolator());
+ return ta;
+ }
+
+ enum AnimationDirection {
+ LEFT, RIGHT
+ }
+}
diff --git a/expanding-collection/src/main/java/com/ramotion/expandingcollection/ECCardContentListItemAdapter.java b/expanding-collection/src/main/java/com/ramotion/expandingcollection/ECCardContentListItemAdapter.java
new file mode 100644
index 0000000..bc47e80
--- /dev/null
+++ b/expanding-collection/src/main/java/com/ramotion/expandingcollection/ECCardContentListItemAdapter.java
@@ -0,0 +1,45 @@
+package com.ramotion.expandingcollection;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+
+import java.util.List;
+
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+/**
+ * Must be implemented to inflate card content list items layout.
+ *
+ * @param Type of items in card content list
+ */
+public abstract class ECCardContentListItemAdapter extends ArrayAdapter {
+ private boolean zeroItemsMode = true;
+
+ public ECCardContentListItemAdapter(@NonNull Context context, @LayoutRes int resource, @NonNull List objects) {
+ super(context, resource, objects);
+ }
+
+ @Override
+ public final int getCount() {
+ return zeroItemsMode ? 0 : super.getCount();
+ }
+
+ @NonNull
+ @Override
+ public abstract View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent);
+
+ protected final void enableZeroItemsMode() {
+ this.zeroItemsMode = true;
+ notifyDataSetChanged();
+ }
+
+ protected final void disableZeroItemsMode() {
+ this.zeroItemsMode = false;
+ notifyDataSetChanged();
+ }
+
+}
diff --git a/expanding-collection/src/main/java/com/ramotion/expandingcollection/ECCardData.java b/expanding-collection/src/main/java/com/ramotion/expandingcollection/ECCardData.java
new file mode 100644
index 0000000..42c2ca3
--- /dev/null
+++ b/expanding-collection/src/main/java/com/ramotion/expandingcollection/ECCardData.java
@@ -0,0 +1,22 @@
+package com.ramotion.expandingcollection;
+
+
+import java.util.List;
+
+import androidx.annotation.DrawableRes;
+
+/**
+ * Implement this interface to provide data to pager view and content list inside pager card
+ *
+ * @param Type of items in card content list
+ */
+public interface ECCardData {
+
+ String getMainBackgroundResource();
+
+ Integer getKey();
+
+ String getHeadBackgroundResource();
+
+ List getListItems();
+}
diff --git a/expanding-collection/src/main/java/com/ramotion/expandingcollection/ECPager.java b/expanding-collection/src/main/java/com/ramotion/expandingcollection/ECPager.java
new file mode 100644
index 0000000..81dd758
--- /dev/null
+++ b/expanding-collection/src/main/java/com/ramotion/expandingcollection/ECPager.java
@@ -0,0 +1,158 @@
+package com.ramotion.expandingcollection;
+
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.FrameLayout;
+
+import androidx.viewpager.widget.PagerAdapter;
+import androidx.viewpager.widget.ViewPager;
+
+/**
+ * Custom ViewPager used as main card pager, neighborhood elements are visible due to {@link ECPagerView},
+ * so this pager element used only inside {@link ECPagerView}. Also pager can change self position and size
+ * for animation purposes.
+ */
+public class ECPager extends ViewPager {
+ private int currentPosition;
+ private boolean pagingDisabled;
+
+ public ECPager(Context context) {
+ super(context);
+ init();
+ }
+
+ public ECPager(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ private void init() {
+ this.setOffscreenPageLimit(3);
+// this.setOverScrollMode(OVER_SCROLL_NEVER);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ return !this.pagingDisabled && super.onTouchEvent(event);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ return !this.pagingDisabled && super.onInterceptTouchEvent(event);
+ }
+
+ public void updateLayoutDimensions(int cardWidth, int cardHeight) {
+ FrameLayout.LayoutParams pagerViewLayoutParams = (FrameLayout.LayoutParams) this.getLayoutParams();
+ pagerViewLayoutParams.height = cardHeight;
+ pagerViewLayoutParams.width = cardWidth;
+ }
+
+ public ECCardData getDataFromAdapterDataset(int position) {
+ return ((ECPagerViewAdapter) this.getAdapter()).getDataset().get(position);
+ }
+
+ public void enablePaging() {
+ this.pagingDisabled = false;
+ }
+
+ public void disablePaging() {
+ this.pagingDisabled = true;
+ }
+
+ @Override
+ public void setAdapter(PagerAdapter adapter) {
+ super.setAdapter(adapter);
+ }
+
+ protected void animateWidth(int targetWidth, int duration, int startDelay, AnimatorListenerAdapter onAnimationEnd) {
+ ValueAnimator pagerWidthAnimation = new ValueAnimator();
+ pagerWidthAnimation.setInterpolator(new AccelerateInterpolator());
+ pagerWidthAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ ViewGroup.LayoutParams pagerLayoutParams = getLayoutParams();
+ pagerLayoutParams.width = (int) animation.getAnimatedValue();
+ setLayoutParams(pagerLayoutParams);
+ }
+ });
+
+ pagerWidthAnimation.setIntValues(getWidth(), targetWidth);
+
+ pagerWidthAnimation.setStartDelay(startDelay);
+ pagerWidthAnimation.setDuration(duration);
+ if (onAnimationEnd != null)
+ pagerWidthAnimation.addListener(onAnimationEnd);
+ pagerWidthAnimation.start();
+ }
+
+ protected void animateHeight(int targetHeight, int duration, int startDelay, AnimatorListenerAdapter onAnimationEnd) {
+ ValueAnimator pagerHeightAnimation = new ValueAnimator();
+ pagerHeightAnimation.setInterpolator(new DecelerateInterpolator());
+ pagerHeightAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ ViewGroup.LayoutParams pagerLayoutParams = getLayoutParams();
+ pagerLayoutParams.height = (int) animation.getAnimatedValue();
+ setLayoutParams(pagerLayoutParams);
+ }
+ });
+
+ pagerHeightAnimation.setIntValues(getHeight(), targetHeight);
+
+ pagerHeightAnimation.setDuration(duration);
+ pagerHeightAnimation.setStartDelay(startDelay);
+ if (onAnimationEnd != null)
+ pagerHeightAnimation.addListener(onAnimationEnd);
+
+ pagerHeightAnimation.start();
+ }
+
+ @Override
+ protected void onPageScrolled(int position, float offset, int offsetPixels) {
+ super.onPageScrolled(position, offset, offsetPixels);
+ }
+
+ /**
+ * Start expand animation for currently active card.
+ *
+ * @return true if animation started
+ */
+ public boolean expand() {
+ ECPagerViewAdapter adapter = (ECPagerViewAdapter) getAdapter();
+ return adapter.getActiveCard().expand();
+ }
+
+ /**
+ * Start collapse animation for currently active card.
+ *
+ * @return true if animation started
+ */
+ public boolean collapse() {
+ ECPagerViewAdapter adapter = (ECPagerViewAdapter) getAdapter();
+ return adapter.getActiveCard().collapse();
+ }
+
+ /**
+ * Toggle state of currently active card - collapse if card is expanded and otherwise
+ *
+ * @return true if animation started
+ */
+ public boolean toggle() {
+ ECPagerViewAdapter adapter = (ECPagerViewAdapter) getAdapter();
+ return adapter.getActiveCard().toggle();
+ }
+
+ public int getCurrentPosition() {
+ return currentPosition;
+ }
+
+ public void setCurrentPosition(int currentPosition) {
+ this.currentPosition = currentPosition;
+ }
+}
diff --git a/expanding-collection/src/main/java/com/ramotion/expandingcollection/ECPagerCard.java b/expanding-collection/src/main/java/com/ramotion/expandingcollection/ECPagerCard.java
new file mode 100644
index 0000000..5840ee9
--- /dev/null
+++ b/expanding-collection/src/main/java/com/ramotion/expandingcollection/ECPagerCard.java
@@ -0,0 +1,137 @@
+package com.ramotion.expandingcollection;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+/**
+ * Pager Card it's simple container that wraps card content list and some logic for work with animations.
+ */
+public class ECPagerCard extends FrameLayout {
+
+ private ECPagerCardContentList ecPagerCardContentList;
+ private boolean animationInProgress;
+ private boolean cardExpanded;
+
+ public ECPagerCard(Context context) {
+ super(context);
+ }
+
+ public ECPagerCard(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public ECPagerCard(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ try {
+ ecPagerCardContentList = (ECPagerCardContentList) getChildAt(0);
+// ecPagerCardListContent.disableScroll();
+ } catch (Exception e) {
+ throw new IllegalStateException("Invalid children element in ECPagerCard.");
+ }
+ }
+
+ /**
+ * Start expand animation.
+ *
+ * @return true if animation started
+ */
+ public boolean expand() {
+ if (animationInProgress || cardExpanded) return false;
+ animationInProgress = true;
+
+ final ECPager pager = (ECPager) getParent();
+ final ECPagerView pagerView = (ECPagerView) pager.getParent();
+
+ pager.disablePaging();
+
+ ViewGroup pagerParent = (ViewGroup) pagerView.getParent();
+ int expandedCardWidth = pagerParent.getWidth();
+ int expandedCardHeight = pagerParent.getHeight();
+
+ AnimatorListenerAdapter onAnimationEnd = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ animationInProgress = false;
+// ecPagerCardContentList.enableScroll();
+ cardExpanded = true;
+ }
+ };
+
+ int pushNeighboursDuration = 200;
+ int cardAnimDelay = 150;
+ int cardAnimDuration = 250;
+
+ pager.animateWidth(expandedCardWidth, pushNeighboursDuration, 0, null);
+
+ ecPagerCardContentList.animateWidth(expandedCardWidth, cardAnimDuration, cardAnimDelay);
+ pagerView.toggleTopMargin(cardAnimDuration, cardAnimDelay);
+ pager.animateHeight(expandedCardHeight, cardAnimDuration, cardAnimDelay, onAnimationEnd);
+ ecPagerCardContentList.getHeadView().animateHeight(pagerView.getCardHeaderExpandedHeight(), cardAnimDuration, cardAnimDelay);
+ ecPagerCardContentList.showListElements();
+ return true;
+ }
+
+ /**
+ * Start collapse animation
+ *
+ * @return true if animation started
+ */
+ public boolean collapse() {
+ if (animationInProgress || !cardExpanded) return false;
+ animationInProgress = true;
+
+ final ECPager pager = (ECPager) getParent();
+ final ECPagerView pagerView = (ECPagerView) pager.getParent();
+
+ pager.disablePaging();
+
+ ecPagerCardContentList.scrollToTop();
+// ecPagerCardListContent.disableScroll();
+
+ AnimatorListenerAdapter onAnimationEnd = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ animationInProgress = false;
+ pager.enablePaging();
+ cardExpanded = false;
+ ecPagerCardContentList.hideListElements();
+ }
+ };
+
+ int cardAnimDuration = 250;
+ int pushNeighboursDelay = 150;
+ int pushNeighboursDuration = 200;
+
+ pagerView.toggleTopMargin(cardAnimDuration, 0);
+ pager.animateHeight(pagerView.getCardHeight(), cardAnimDuration, 0, null);
+ ecPagerCardContentList.animateWidth(pagerView.getCardWidth(), cardAnimDuration, 0);
+ ecPagerCardContentList.getHeadView().animateHeight(pagerView.getCardHeight(), cardAnimDuration, 0);
+
+ pager.animateWidth(pagerView.getCardWidth(), pushNeighboursDuration, pushNeighboursDelay, onAnimationEnd);
+ return true;
+ }
+
+ /**
+ * Toggle state of card - collapse if card is expanded and otherwise
+ *
+ * @return true if animation started
+ */
+ public boolean toggle() {
+ if (cardExpanded)
+ return collapse();
+ else return expand();
+ }
+
+ public ECPagerCardContentList getEcPagerCardContentList() {
+ return ecPagerCardContentList;
+ }
+}
diff --git a/expanding-collection/src/main/java/com/ramotion/expandingcollection/ECPagerCardContentList.java b/expanding-collection/src/main/java/com/ramotion/expandingcollection/ECPagerCardContentList.java
new file mode 100644
index 0000000..0ab65a8
--- /dev/null
+++ b/expanding-collection/src/main/java/com/ramotion/expandingcollection/ECPagerCardContentList.java
@@ -0,0 +1,131 @@
+package com.ramotion.expandingcollection;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Color;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.AbsListView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+
+/**
+ * Card content list it's main element of card content - just a list view with custom header and animations.
+ */
+public class ECPagerCardContentList extends ListView {
+
+ private boolean scrollDisabled;
+ private int mPosition;
+
+ private ECCardContentListItemAdapter contentListItemAdapter;
+
+ private ECPagerCardHead headView;
+
+ public ECPagerCardContentList(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public ECPagerCardContentList(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ public ECPagerCardContentList(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context);
+ }
+
+ public void init(Context context) {
+ headView = new ECPagerCardHead(context);
+ headView.setBackgroundColor(Color.RED);
+ headView.setLayoutParams(new AbsListView.LayoutParams(AbsListView.LayoutParams.MATCH_PARENT, AbsListView.LayoutParams.WRAP_CONTENT));
+ addHeaderView(headView);
+// this.setOverScrollMode(OVER_SCROLL_NEVER);
+ }
+
+ public ECCardContentListItemAdapter getContentListItemAdapter() {
+ return contentListItemAdapter;
+ }
+
+ @Override
+ public void setAdapter(ListAdapter adapter) {
+ super.setAdapter(adapter);
+ if (adapter instanceof ECCardContentListItemAdapter) {
+ this.contentListItemAdapter = (ECCardContentListItemAdapter) adapter;
+ }
+ }
+
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ final int actionMasked = ev.getActionMasked() & MotionEvent.ACTION_MASK;
+ // Ignore move events if scroll disabled
+ if (scrollDisabled && actionMasked == MotionEvent.ACTION_MOVE) {
+ return true;
+ }
+ // Ignore scroll events if scroll disabled
+ if (scrollDisabled && actionMasked == MotionEvent.ACTION_SCROLL) {
+ return true;
+ }
+ // Save the event initial position
+ if (actionMasked == MotionEvent.ACTION_DOWN) {
+ mPosition = pointToPosition((int) ev.getX(), (int) ev.getY());
+ return super.dispatchTouchEvent(ev);
+ }
+ // Check if we are still in the same position, otherwise cancel event
+ int eventPosition = pointToPosition((int) ev.getX(), (int) ev.getY());
+ if (actionMasked == MotionEvent.ACTION_UP) {
+ if (eventPosition != mPosition) {
+ ev.setAction(MotionEvent.ACTION_CANCEL);
+ }
+ }
+ return super.dispatchTouchEvent(ev);
+ }
+
+ public void disableScroll() {
+ scrollDisabled = true;
+ }
+
+ public void enableScroll() {
+ scrollDisabled = false;
+ }
+
+ public void scrollToTop() {
+ this.smoothScrollToPosition(0);
+ }
+
+ protected ECPagerCardHead getHeadView() {
+ return headView;
+ }
+
+ protected void animateWidth(int targetWidth, int duration, int delay) {
+ // reset own width for smooth animation and avoid values like 'MATCH_PARENT'
+ this.getLayoutParams().width = this.getWidth();
+
+ ValueAnimator widthAnimation = new ValueAnimator();
+ widthAnimation.setInterpolator(new DecelerateInterpolator());
+ widthAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ ViewGroup.LayoutParams pagerLayoutParams = getLayoutParams();
+ pagerLayoutParams.width = (int) animation.getAnimatedValue();
+ setLayoutParams(pagerLayoutParams);
+ }
+ });
+ widthAnimation.setIntValues(getWidth(), targetWidth);
+ widthAnimation.setStartDelay(delay);
+ widthAnimation.setDuration(duration);
+ widthAnimation.start();
+ }
+
+ protected final void hideListElements() {
+ getContentListItemAdapter().enableZeroItemsMode();
+ }
+
+ protected final void showListElements() {
+ getContentListItemAdapter().disableZeroItemsMode();
+ }
+
+}
diff --git a/expanding-collection/src/main/java/com/ramotion/expandingcollection/ECPagerCardHead.java b/expanding-collection/src/main/java/com/ramotion/expandingcollection/ECPagerCardHead.java
new file mode 100644
index 0000000..55e1092
--- /dev/null
+++ b/expanding-collection/src/main/java/com/ramotion/expandingcollection/ECPagerCardHead.java
@@ -0,0 +1,101 @@
+package com.ramotion.expandingcollection;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.AbsListView;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import androidx.annotation.DrawableRes;
+
+import com.bumptech.glide.Glide;
+
+
+/**
+ * View used to be a card head that is visible when card is collapsed. Just frame layout with background image
+ * and logic for animate height.
+ */
+public class ECPagerCardHead extends FrameLayout {
+
+ private ImageView headBackgroundImageView;
+
+ public ECPagerCardHead(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public ECPagerCardHead(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ public ECPagerCardHead(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context);
+ }
+
+ public void init(Context context) {
+// headBackgroundImageView = new TopCropImageView(context);
+ headBackgroundImageView = new ImageView(context);
+ headBackgroundImageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
+ LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT, AbsListView.LayoutParams.MATCH_PARENT);
+ headBackgroundImageView.setLayoutParams(params);
+ this.addView(headBackgroundImageView);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ try {
+ headBackgroundImageView = (ImageView) getChildAt(0);
+ } catch (Exception e) {
+ throw new IllegalStateException("Invalid children elements in ECPagerCardHead.");
+ }
+ }
+
+ protected void setHeadImageDrawable(Drawable headImageDrawable) {
+ if (this.headBackgroundImageView != null)
+ this.headBackgroundImageView.setImageDrawable(headImageDrawable);
+ }
+
+ protected void setHeadImageDrawable(@DrawableRes int headImageDrawableRes) {
+ if (this.headBackgroundImageView != null)
+ this.headBackgroundImageView.setImageResource(headImageDrawableRes);
+ }
+
+ protected void setHeadImageBitmap(String headImageBitmap) {
+ if (this.headBackgroundImageView != null)
+ Glide.with(getContext()).load(headImageBitmap).into(this.headBackgroundImageView);
+ // this.headBackgroundImageView.setImageBitmap(headImageBitmap);
+ }
+
+ protected void animateHeight(int targetHeight, int duration, int delay) {
+ final ViewGroup.LayoutParams cardHeaderLayoutParams = this.getLayoutParams();
+
+ ValueAnimator cardHeadHeightAnimation = new ValueAnimator();
+ cardHeadHeightAnimation.setInterpolator(new DecelerateInterpolator());
+ cardHeadHeightAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ cardHeaderLayoutParams.height = (int) animation.getAnimatedValue();
+ ECPagerCardHead.this.setLayoutParams(cardHeaderLayoutParams);
+ }
+ });
+
+ cardHeadHeightAnimation.setIntValues(cardHeaderLayoutParams.height, targetHeight);
+
+ cardHeadHeightAnimation.setDuration(duration);
+ cardHeadHeightAnimation.setStartDelay(delay);
+ cardHeadHeightAnimation.start();
+ }
+
+ protected void setHeight(int height) {
+ this.getLayoutParams().height = height;
+ }
+
+}
diff --git a/expanding-collection/src/main/java/com/ramotion/expandingcollection/ECPagerView.java b/expanding-collection/src/main/java/com/ramotion/expandingcollection/ECPagerView.java
new file mode 100644
index 0000000..35e8e40
--- /dev/null
+++ b/expanding-collection/src/main/java/com/ramotion/expandingcollection/ECPagerView.java
@@ -0,0 +1,284 @@
+package com.ramotion.expandingcollection;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Point;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.FrameLayout;
+import android.widget.RelativeLayout;
+
+import java.util.List;
+
+import androidx.viewpager.widget.ViewPager;
+import ramotion.com.expandingcollection.R;
+
+/**
+ * Root PagerView element. Wraps all logic, animations and behavior.
+ */
+public class ECPagerView extends FrameLayout implements ViewPager.OnPageChangeListener {
+ private ECPager pager;
+ private ECBackgroundSwitcherView attachedImageSwitcher;
+ private OnCardSelectedListener onCardSelectedListener;
+
+ private boolean needsRedraw;
+ private int nextTopMargin = 0;
+
+ private Point center = new Point();
+ private Point initialTouch = new Point();
+
+ private Integer cardWidth;
+ private Integer cardHeight;
+ private Integer cardHeaderExpandedHeight;
+
+ public ECPagerView(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public ECPagerView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initializeFromAttributes(context, attrs);
+ init(context);
+ }
+
+ public ECPagerView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ initializeFromAttributes(context, attrs);
+ init(context);
+ }
+
+ protected void initializeFromAttributes(Context context, AttributeSet attrs) {
+ TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ExpandingCollection, 0, 0);
+ try {
+ this.cardWidth = array.getDimensionPixelSize(R.styleable.ExpandingCollection_cardWidth, 500);
+ this.cardHeight = array.getDimensionPixelSize(R.styleable.ExpandingCollection_cardHeight, 550);
+ this.cardHeaderExpandedHeight = array.getDimensionPixelSize(R.styleable.ExpandingCollection_cardHeaderHeightExpanded, 450);
+ } finally {
+ array.recycle();
+ }
+ }
+
+ private void init(Context context) {
+ setClipChildren(false);
+ setClipToPadding(false);
+
+ if (Build.VERSION.SDK_INT < 21)
+ setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+
+ pager = new ECPager(context);
+
+ LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ layoutParams.gravity = Gravity.CENTER;
+ this.addView(pager, 0, layoutParams);
+
+ pager.setPageTransformer(false, new AlphaScalePageTransformer());
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ try {
+ pager = (ECPager) getChildAt(0);
+ pager.addOnPageChangeListener(this);
+ pager.updateLayoutDimensions(cardWidth, cardHeight);
+ } catch (Exception e) {
+ throw new IllegalStateException("The root child of PagerContainer must be a ViewPager");
+ }
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ center.x = w / 2;
+ center.y = h / 2;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ // We capture any touches not already handled by the ViewPager
+ // to implement scrolling from a touch outside the pager bounds.
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ initialTouch.x = (int) ev.getX();
+ initialTouch.y = (int) ev.getY();
+ default:
+ ev.offsetLocation(center.x - initialTouch.x, center.y - initialTouch.y);
+ break;
+ }
+ return pager.dispatchTouchEvent(ev);
+ }
+
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+ //Force the container to redraw on scrolling.
+ //Without this the outer pages render initially and then stay static
+ if (needsRedraw) invalidate();
+ }
+
+ @Override
+ public void onPageSelected(int position) {
+ int oldPosition = pager.getCurrentPosition();
+ pager.setCurrentPosition(position);
+
+ ECBackgroundSwitcherView.AnimationDirection direction = null;
+ int nextPositionPrediction = position;
+ if (oldPosition < position) {
+ direction = ECBackgroundSwitcherView.AnimationDirection.LEFT;
+ nextPositionPrediction++;
+ } else if (oldPosition > position) {
+ direction = ECBackgroundSwitcherView.AnimationDirection.RIGHT;
+ nextPositionPrediction--;
+ }
+
+ if (attachedImageSwitcher != null) {
+ attachedImageSwitcher.setReverseDrawOrder(attachedImageSwitcher.getDisplayedChild() == 1);
+
+ // change current image from cache or reinitialize it from resource
+ BackgroundBitmapCache instance = BackgroundBitmapCache.getInstance();
+ Integer mainBgImageDrawableResource = pager.getDataFromAdapterDataset(position).getKey();
+ if (instance.getBitmapFromBgMemCache(mainBgImageDrawableResource) != null) {
+ attachedImageSwitcher.updateCurrentBackground(pager, direction);
+ } else {
+ attachedImageSwitcher.updateCurrentBackgroundAsync(pager, direction);
+ }
+ // change background on next predicted position
+ //attachedImageSwitcher.cacheBackgroundAtPosition(pager, nextPositionPrediction);
+ }
+ if (onCardSelectedListener != null)
+ onCardSelectedListener.cardSelected(position, oldPosition, pager.getAdapter().getCount());
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+ needsRedraw = (state != ViewPager.SCROLL_STATE_IDLE);
+ }
+
+ protected void toggleTopMargin(int duration, int delay) {
+ final RelativeLayout.LayoutParams containerLayoutParams = (RelativeLayout.LayoutParams) this.getLayoutParams();
+ int currentTopMargin = containerLayoutParams.topMargin;
+ ValueAnimator marginAnimation = new ValueAnimator();
+ marginAnimation.setInterpolator(new DecelerateInterpolator());
+ marginAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ containerLayoutParams.topMargin = (int) animation.getAnimatedValue();
+ ECPagerView.this.setLayoutParams(containerLayoutParams);
+ }
+ });
+ marginAnimation.setIntValues(containerLayoutParams.topMargin, nextTopMargin);
+ marginAnimation.setDuration(duration);
+ marginAnimation.setStartDelay(delay);
+ marginAnimation.start();
+ nextTopMargin = currentTopMargin;
+ }
+
+ /**
+ * Attach {@link ECBackgroundSwitcherView} element to pager view
+ *
+ * @param imageSwitcher already inflated {@link ECBackgroundSwitcherView} element
+ */
+ public void setBackgroundSwitcherView(ECBackgroundSwitcherView imageSwitcher) {
+ this.attachedImageSwitcher = imageSwitcher;
+ if (imageSwitcher == null) return;
+ ECPagerViewAdapter adapter = (ECPagerViewAdapter) this.pager.getAdapter();
+ if (adapter != null && adapter.getDataset() != null && adapter.getDataset().size() > 1) {
+ attachedImageSwitcher.updateCurrentBackground(pager, null);
+ }
+ }
+
+ /**
+ * Set {@link ECPagerViewAdapter} to pager
+ *
+ * @param adapter implementation of {@link ECPagerViewAdapter}
+ */
+ public void setPagerViewAdapter(ECPagerViewAdapter adapter) {
+ this.pager.setAdapter(adapter);
+ if (adapter == null) return;
+ List dataset = adapter.getDataset();
+ if (dataset != null && dataset.size() > 1 && attachedImageSwitcher != null) {
+ attachedImageSwitcher.updateCurrentBackground(pager, null);
+ }
+ if (pager.getAdapter() != null && onCardSelectedListener != null)
+ onCardSelectedListener.cardSelected(pager.getCurrentPosition(), pager.getCurrentPosition(), pager.getAdapter().getCount());
+ }
+
+ /**
+ * Tune parameters of PagerView element.
+ *
+ * @param cardWidth width of card in collapsed state
+ * @param cardHeight height of card in collapsed state
+ * @param cardHeaderExpandedHeight height of card header in expanded state
+ */
+ public void setAttributes(int cardWidth, int cardHeight, int cardHeaderExpandedHeight) {
+ this.cardWidth = cardWidth;
+ this.cardHeight = cardHeight;
+ this.cardHeaderExpandedHeight = cardHeaderExpandedHeight;
+ this.pager.updateLayoutDimensions(cardWidth, cardHeight);
+ }
+
+ /**
+ * Set {@link OnCardSelectedListener} to pager view.
+ *
+ * @param listener
+ */
+ public void setOnCardSelectedListener(OnCardSelectedListener listener) {
+ this.onCardSelectedListener = listener;
+ if (listener == null) return;
+ if (pager.getAdapter() != null)
+ onCardSelectedListener.cardSelected(pager.getCurrentPosition(), pager.getCurrentPosition(), pager.getAdapter().getCount());
+
+ }
+
+ public int getCardWidth() {
+ return cardWidth;
+ }
+
+ public int getCardHeight() {
+ return cardHeight;
+ }
+
+ public int getCardHeaderExpandedHeight() {
+ return cardHeaderExpandedHeight;
+ }
+
+ /**
+ * Start expand animation for currently active card.
+ *
+ * @return true if animation started
+ */
+ public boolean expand() {
+ return pager.expand();
+ }
+
+ /**
+ * Start collapse animation for currently active card.
+ *
+ * @return true if animation started
+ */
+ public boolean collapse() {
+ return pager.collapse();
+ }
+
+ /**
+ * Toggle state of currently active card - collapse if card is expanded and otherwise
+ *
+ * @return true if animation started
+ */
+ public boolean toggle() {
+ return pager.toggle();
+ }
+
+ /**
+ * Listener will be notified when pager select a new card
+ */
+ public interface OnCardSelectedListener {
+ void cardSelected(int newPosition, int oldPosition, int totalElements);
+ }
+
+}
diff --git a/expanding-collection/src/main/java/com/ramotion/expandingcollection/ECPagerViewAdapter.java b/expanding-collection/src/main/java/com/ramotion/expandingcollection/ECPagerViewAdapter.java
new file mode 100644
index 0000000..887b73a
--- /dev/null
+++ b/expanding-collection/src/main/java/com/ramotion/expandingcollection/ECPagerViewAdapter.java
@@ -0,0 +1,98 @@
+package com.ramotion.expandingcollection;
+
+import android.content.Context;
+import android.graphics.BitmapFactory;
+import android.os.StrictMode;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.List;
+
+import androidx.viewpager.widget.PagerAdapter;
+import ramotion.com.expandingcollection.R;
+
+import static android.R.attr.bitmap;
+
+/**
+ * Adapter must be implemented to provide your layouts and data(that implements {@link ECCardData})
+ * to cards in {@link ECPagerView}.
+ */
+public abstract class ECPagerViewAdapter extends PagerAdapter {
+
+ private ECPagerCard activeCard;
+ private List dataset;
+ private LayoutInflater inflaterService;
+
+ public ECPagerViewAdapter(Context applicationContext, List dataset) {
+ this.inflaterService = (LayoutInflater) applicationContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ this.dataset = dataset;
+ }
+
+ @Override
+ public Object instantiateItem(ViewGroup container, int position) {
+ final ECPager pager = (ECPager) container;
+ final ECPagerCard pagerCard = (ECPagerCard) inflaterService.inflate(R.layout.ec_pager_card, null);
+ final ECPagerView pagerContainer = (ECPagerView) pager.getParent();
+
+ ECPagerCardContentList ecPagerCardContentList = pagerCard.getEcPagerCardContentList();
+ ECPagerCardHead headView = ecPagerCardContentList.getHeadView();
+
+ headView.setHeight(pagerContainer.getCardHeight());
+
+ String drawableRes = dataset.get(position).getHeadBackgroundResource();
+ headView.setHeadImageBitmap(drawableRes);
+ /* if (drawableRes != null) {
+ StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
+ StrictMode.setThreadPolicy(policy);
+ try {
+ URL url = new URL(drawableRes);
+ headView.setHeadImageBitmap(BitmapFactory.decodeStream((InputStream)url.getContent()));
+ } catch (IOException e) {
+ //Log.e(TAG, e.getMessage());
+ }
+ //headView.setHeadImageBitmap(BitmapFactory.decodeResource(pagerContainer.getResources(), drawableRes, new BitmapFactoryOptions()));
+ }*/
+
+ instantiateCard(inflaterService, headView, ecPagerCardContentList, dataset.get(position));
+
+ pager.addView(pagerCard, pagerContainer.getCardWidth(), pagerContainer.getCardHeight());
+ return pagerCard;
+ }
+
+
+ public abstract void instantiateCard(LayoutInflater inflaterService, ViewGroup head, ListView list, ECCardData data);
+
+ @Override
+ public void destroyItem(ViewGroup container, int position, Object object) {
+ container.removeView((View) object);
+ }
+
+ @Override
+ public void setPrimaryItem(ViewGroup container, int position, Object object) {
+ super.setPrimaryItem(container, position, object);
+ activeCard = (ECPagerCard) object;
+ }
+
+ public ECPagerCard getActiveCard() {
+ return activeCard;
+ }
+
+ @Override
+ public int getCount() {
+ return dataset.size();
+ }
+
+ @Override
+ public boolean isViewFromObject(View view, Object object) {
+ return (view == object);
+ }
+
+ public List getDataset() {
+ return dataset;
+ }
+}
diff --git a/expanding-collection/src/main/java/com/ramotion/expandingcollection/GlideAppGenModule.java b/expanding-collection/src/main/java/com/ramotion/expandingcollection/GlideAppGenModule.java
new file mode 100644
index 0000000..775692f
--- /dev/null
+++ b/expanding-collection/src/main/java/com/ramotion/expandingcollection/GlideAppGenModule.java
@@ -0,0 +1,8 @@
+package com.ramotion.expandingcollection;
+
+import com.bumptech.glide.annotation.GlideModule;
+import com.bumptech.glide.module.AppGlideModule;
+
+/*@GlideModule*/
+public class GlideAppGenModule extends AppGlideModule {
+}
diff --git a/expanding-collection/src/main/java/com/ramotion/expandingcollection/TopCropImageView.java b/expanding-collection/src/main/java/com/ramotion/expandingcollection/TopCropImageView.java
new file mode 100644
index 0000000..0449362
--- /dev/null
+++ b/expanding-collection/src/main/java/com/ramotion/expandingcollection/TopCropImageView.java
@@ -0,0 +1,37 @@
+package com.ramotion.expandingcollection;
+
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+
+import androidx.appcompat.widget.AppCompatImageView;
+
+/**
+ * Image View with custom content crop logic.
+ */
+public class TopCropImageView extends AppCompatImageView {
+ public TopCropImageView(Context context) {
+ super(context);
+ setScaleType(ScaleType.MATRIX);
+ }
+
+ public TopCropImageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setScaleType(ScaleType.MATRIX);
+ }
+
+ @Override
+ protected boolean setFrame(int l, int t, int r, int b) {
+ boolean changed = super.setFrame(l, t, r, b);
+ Drawable drawable = getDrawable();
+ if (drawable != null) {
+ Matrix matrix = getImageMatrix();
+ float scaleFactor = getWidth() / (float) drawable.getIntrinsicWidth();
+ matrix.setScale(scaleFactor, scaleFactor, 0, 0);
+ setImageMatrix(matrix);
+ }
+ return changed;
+ }
+
+}
diff --git a/expanding-collection/src/main/res/layout/ec_pager_card.xml b/expanding-collection/src/main/res/layout/ec_pager_card.xml
new file mode 100644
index 0000000..bf329fc
--- /dev/null
+++ b/expanding-collection/src/main/res/layout/ec_pager_card.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/expanding-collection/src/main/res/values/attrs.xml b/expanding-collection/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..b54252e
--- /dev/null
+++ b/expanding-collection/src/main/res/values/attrs.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/expanding-collection/src/main/res/values/strings.xml b/expanding-collection/src/main/res/values/strings.xml
new file mode 100644
index 0000000..8143889
--- /dev/null
+++ b/expanding-collection/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ expanding-collection
+
diff --git a/expanding-collection/src/test/java/ramotion/com/expandingcollection/ExampleUnitTest.java b/expanding-collection/src/test/java/ramotion/com/expandingcollection/ExampleUnitTest.java
new file mode 100644
index 0000000..be6da2f
--- /dev/null
+++ b/expanding-collection/src/test/java/ramotion/com/expandingcollection/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package ramotion.com.expandingcollection;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() throws Exception {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file