package moe.codeest.ecardflow; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.TransitionDrawable; import android.os.Build; import android.os.Handler; import android.os.Message; import android.renderscript.RSRuntimeException; import android.support.annotation.Nullable; import android.support.v4.util.LruCache; import android.support.v4.view.ViewPager; import android.util.AttributeSet; import android.util.Log; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; import java.lang.ref.WeakReference; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import moe.codeest.ecardflow.mode.AnimMode; import moe.codeest.ecardflow.mode.BlurAnimMode; import moe.codeest.ecardflow.provider.ImageProvider; import moe.codeest.ecardflow.support.RecyclingBitmapDrawable; import moe.codeest.ecardflow.util.FastBlur; import moe.codeest.ecardflow.util.RSBlur; /** * Created by codeest on 2017/1/16. */ public class ECardFlowLayout extends FrameLayout{ private static final int SWITCH_ANIM_TIME = 300; //300 ms private static final int MSG_JUDGE_RESET = 0x1; private Context mContext; private ExecutorService mThreadPool; private LruCache<String, RecyclingBitmapDrawable> mLruCache; private MyHandler mHandler; private NotifyRunnable mNotifyRunnable; private ImageView mBgImage; private ImageView mBlurImage; private ViewPager mViewPager; private ImageProvider mProvider; private AnimMode mAnimMode; private RecyclingBitmapDrawable curBp, lastBp, nextBp; private Bitmap curBlurBp, lastBlurBp, nextBlurBp; private int mCurDirection = AnimMode.D_RIGHT; private int mSwitchAnimTime = SWITCH_ANIM_TIME; private int mMinCacheSize; private float mLastOffset; private int mRadius; private int mCurPosition; private int mLastPosition; private boolean isSwitching; public ECardFlowLayout(Context context) { super(context); } public ECardFlowLayout(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.attr_layout); mSwitchAnimTime = ta.getInt(R.styleable.attr_layout_switchAnimTime, SWITCH_ANIM_TIME); ta.recycle(); init(); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); initViewPager(); } private void init() { mThreadPool = Executors.newCachedThreadPool(); mNotifyRunnable = new NotifyRunnable(); mHandler = new MyHandler(this); initCache(); mBlurImage = new ImageView(mContext); initImageView(mBlurImage); mBgImage = new ImageView(mContext); initImageView(mBgImage); } private void initImageView(ImageView image) { image.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); image.setScaleType(ImageView.ScaleType.CENTER_CROP); addView(image); } private void initViewPager() { for (int i= 0; i< getChildCount(); i++) { if (getChildAt(i) instanceof ViewPager) { mViewPager = (ViewPager) getChildAt(i); } } if (mViewPager == null) { throw new IllegalStateException("Can't find ViewPager in ECardFlowLayout"); } mViewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { if (positionOffset != 0 && positionOffset != mLastOffset) { if (positionOffset < mLastOffset) { mCurDirection = AnimMode.D_LEFT; } else { mCurDirection = AnimMode.D_RIGHT; } mLastOffset = positionOffset; } int lastPosition = Math.round(position + positionOffset); if (mLastPosition != lastPosition) { if (mCurDirection == AnimMode.D_LEFT) { switchBgToLast(lastPosition); } else { switchBgToNext(lastPosition); } } mLastPosition = lastPosition; if (mAnimMode != null) { mAnimMode.transformPage(mBgImage, position + positionOffset, mCurDirection); } } @Override public void onPageSelected(int position) { } @Override public void onPageScrollStateChanged(int state) { } }); } private void initCache() { int maxMemory = (int) Runtime.getRuntime().maxMemory() / 1024; int cacheSize = maxMemory / 5; mMinCacheSize = maxMemory / 8; mLruCache = new LruCache<String, RecyclingBitmapDrawable>(cacheSize) { @Override protected int sizeOf(String key, RecyclingBitmapDrawable value) { return value.getBitmap().getByteCount() / 1024; } @Override protected void entryRemoved(boolean evicted, String key, RecyclingBitmapDrawable oldValue, RecyclingBitmapDrawable newValue) { super.entryRemoved(evicted, key, oldValue, newValue); if (evicted && oldValue != null) { oldValue.setIsCached(false); } } @Override protected RecyclingBitmapDrawable create(String key) { RecyclingBitmapDrawable bitmap = new RecyclingBitmapDrawable(getResources(), mProvider.onProvider(Integer.valueOf(key))); if (bitmap.getBitmap() == null) return null; bitmap.setIsCached(true); return bitmap; } }; } private void updateNextRes(final int position) { mCurPosition = position; detachBitmap(lastBp); lastBp = curBp; curBp = nextBp; if (mBlurImage != null) { recycleBitmap(lastBlurBp); lastBlurBp = curBlurBp; curBlurBp = nextBlurBp; } if (mProvider != null) { mThreadPool.execute(new Runnable() { @Override public void run() { nextBp = loadBitmap(position + 1); if (mBlurImage != null) { nextBlurBp = blurBitmap(position + 1); } sendMsg(); } }); } else { throw new RuntimeException("setImageProvider is necessary"); } } private void updateLastRes(final int position) { mCurPosition = position; detachBitmap(nextBp); nextBp = curBp; curBp = lastBp; if (mBlurImage != null) { recycleBitmap(nextBlurBp); nextBlurBp = curBlurBp; curBlurBp = lastBlurBp; } if (mProvider != null) { mThreadPool.execute(new Runnable() { @Override public void run() { lastBp = loadBitmap(position - 1); if (mBlurImage != null) { lastBlurBp = blurBitmap(position - 1); } sendMsg(); } }); } else { throw new IllegalArgumentException("setImageProvider is necessary"); } } private void startTrans(int targetPosition, ImageView targetImage, RecyclingBitmapDrawable startBp, RecyclingBitmapDrawable endBp) { if (endBp == null) endBp = loadBitmap(targetPosition); TransitionDrawable td = new TransitionDrawable(new Drawable[] {startBp, endBp}); targetImage.setImageDrawable(td); td.setCrossFadeEnabled(true); td.startTransition(mSwitchAnimTime); } private void switchBgToNext(final int targetPosition) { if (isSwitching) { return; } isSwitching = true; startTrans(targetPosition + 1, mBgImage, curBp, nextBp); if (mBlurImage != null) { startTrans(targetPosition + 1, mBlurImage, new RecyclingBitmapDrawable(getResources(), curBlurBp), new RecyclingBitmapDrawable(getResources(), nextBlurBp)); } mNotifyRunnable.setTarget(targetPosition, true); mBgImage.postDelayed(mNotifyRunnable, mSwitchAnimTime); } private void switchBgToLast(final int targetPosition) { if (isSwitching) { return; } isSwitching = true; startTrans(targetPosition - 1, mBgImage, curBp, lastBp); if (mBlurImage != null) { startTrans(targetPosition - 1, mBlurImage, new RecyclingBitmapDrawable(getResources(), curBlurBp), new RecyclingBitmapDrawable(getResources(), lastBlurBp)); } mNotifyRunnable.setTarget(targetPosition, false); mBgImage.postDelayed(mNotifyRunnable, mSwitchAnimTime); } private void jumpBgToTarget(final int targetPosition) { mCurPosition = targetPosition; if (isSwitching) { return; } isSwitching = true; final RecyclingBitmapDrawable newBitmap = loadBitmap(targetPosition); TransitionDrawable td = new TransitionDrawable(new Drawable[] {curBp, newBitmap}); mBgImage.setImageDrawable(td); td.setCrossFadeEnabled(true); td.startTransition(mSwitchAnimTime); if (mBlurImage != null) { TransitionDrawable tdb = new TransitionDrawable(new Drawable[] {new BitmapDrawable(mContext.getResources(), curBlurBp), new BitmapDrawable(mContext.getResources(), blurBitmap(targetPosition))}); mBlurImage.setImageDrawable(tdb); tdb.setCrossFadeEnabled(true); tdb.startTransition(mSwitchAnimTime); } mBgImage.postDelayed(new Runnable() { @Override public void run() { mThreadPool.execute(new Runnable() { @Override public void run() { detachBitmap(nextBp); detachBitmap(lastBp); detachBitmap(curBp); curBp = newBitmap; nextBp = loadBitmap(targetPosition + 1); lastBp = loadBitmap(targetPosition - 1); if (mBlurImage != null) { recycleBitmap(nextBlurBp); recycleBitmap(lastBlurBp); recycleBitmap(curBlurBp); curBlurBp = blurBitmap(targetPosition); nextBlurBp = blurBitmap(targetPosition + 1); lastBlurBp = blurBitmap(targetPosition - 1); } sendMsg(); } }); } }, mSwitchAnimTime); } private void sendMsg() { Message msg = new Message(); msg.what = MSG_JUDGE_RESET; if (mHandler != null) { mHandler.sendMessage(msg); } } private void judgeReset() { isSwitching = false; if (Math.abs(mCurPosition - mLastPosition) <= 1) { if (mCurPosition > mLastPosition) { switchBgToLast(mLastPosition); } else if (mCurPosition < mLastPosition) { switchBgToNext(mLastPosition); } } else { jumpBgToTarget(mLastPosition); } } @Nullable private Bitmap blurBitmap(int targetPosition) { RecyclingBitmapDrawable bitmapDrawable = mLruCache.get(String.valueOf(targetPosition)); if (bitmapDrawable == null || bitmapDrawable.getBitmap() == null) { return null; } Bitmap blurBitmap = bitmapDrawable.getBitmap().copy(Bitmap.Config.ARGB_8888, true); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { try { blurBitmap = RSBlur.blur(mContext, blurBitmap, mRadius); } catch (RSRuntimeException e) { blurBitmap = FastBlur.blur(blurBitmap, mRadius, true); } } else { blurBitmap = FastBlur.blur(blurBitmap, mRadius, true); } return blurBitmap; } private RecyclingBitmapDrawable loadBitmap(int targetPosition) { RecyclingBitmapDrawable bitmap = mLruCache.get(String.valueOf(targetPosition)); if (bitmap != null) { bitmap.setIsDisplayed(true); } return bitmap; } private void recycleBitmap(Bitmap bitmap) { if (bitmap != null) bitmap.recycle(); } private void detachBitmap(RecyclingBitmapDrawable bitmap) { if (bitmap != null) { bitmap.setIsDisplayed(false); } } public void setImageProvider(ImageProvider provider) { mProvider = provider; curBp = loadBitmap(0); nextBp = loadBitmap(1); if (mAnimMode == null) { throw new IllegalStateException("You should setAnimMode before setImageProvider"); } if (mBlurImage != null) { curBlurBp = blurBitmap(0); nextBlurBp = blurBitmap(1); mBlurImage.setImageBitmap(blurBitmap(0)); } mBgImage.setImageBitmap(curBp.getBitmap()); } public void setAnimMode(AnimMode animMode) { mAnimMode = animMode; if (!(mAnimMode instanceof BlurAnimMode)) { removeView(mBlurImage); mBlurImage = null; } else { mRadius = ((BlurAnimMode) mAnimMode).getBlurRadius(); } } public void setSwitchAnimTime(int switchAnimTime) { mSwitchAnimTime = switchAnimTime; } public void setCacheSize(int megabytes) { if (megabytes * 1024 >= mMinCacheSize) { mLruCache.resize(megabytes * 1024); } else { Log.w(getClass().getName(), "Size is too small to resize"); } } public void onDestroy() { mHandler.removeCallbacksAndMessages(null); if (!mThreadPool.isShutdown()) { mThreadPool.shutdown(); } mHandler = null; detachBitmap(curBp); detachBitmap(lastBp); detachBitmap(nextBp); mLruCache.evictAll(); if (mBlurImage != null) { recycleBitmap(curBlurBp); recycleBitmap(lastBlurBp); recycleBitmap(nextBlurBp); } } private class NotifyRunnable implements Runnable { private int targetPosition; private boolean isNext; @Override public void run() { if (isNext) { updateNextRes(targetPosition); } else { updateLastRes(targetPosition); } } void setTarget(int targetPosition, boolean isNext) { this.targetPosition = targetPosition; this.isNext = isNext; } } private static class MyHandler extends Handler { WeakReference<ECardFlowLayout> mLayout; MyHandler(ECardFlowLayout layout) { mLayout = new WeakReference<>(layout); } @Override public void handleMessage(Message msg) { ECardFlowLayout layout = mLayout.get(); switch (msg.what) { case MSG_JUDGE_RESET: layout.judgeReset(); break; } } } }