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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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