卡片折叠效果

mac2025-07-06  8

应项目需求,参考网络资源并整理成适合当前项目所用,为防止二次重复整理,特在此记录一笔。

这里主要提供的是一个可折叠的VIewGroup控件

如果你有以下需求,那么本文章或许可以帮到你: 1、需要折叠某一个View或者ViewGroup 2、支持左右对折或者上下对折 3、可以动态控制对折幅度 4、可以随意给对折过程中增加些简单效果,例如缩放或者改变透明度 5、一键折叠或一键展开

Demo下载链接 效果截图如下:

核心代码原作者开源地址(需要梯子) 视频解说:DevBytes: Folding Layout - YouTube

这位作者比较详细的分析了源码实现,有兴趣的可以查看:https://blog.csdn.net/wangjinyu501/article/details/24289861

以下为代码区域,想直接上传文件奈何并不支持,只能用代码块包裹了

在XML布局中的使用

<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:id="@+id/btnOpen" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="展开" android:layout_centerHorizontal="true" android:layout_alignParentBottom="true" /> <liming.growthroad.fun.animation.cardfold.FoldingLayout android:id="@+id/fold_view" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_centerInParent="true" android:scaleX="0.1" android:scaleY="0.1" > <!-- 这里就是被折叠的内容 --> <RelativeLayout android:layout_width="match_parent" android:layout_height="match_parent"> <ImageView android:id="@+id/image_view" android:layout_height="match_parent" android:layout_width="match_parent" android:background="@drawable/banner" android:scaleType="fitXY"/> <Button android:id="@+id/btnFold" android:layout_width="match_parent" android:layout_height="200dp" android:text="关闭" android:layout_centerInParent="true" /> </RelativeLayout> </liming.growthroad.fun.animation.cardfold.FoldingLayout> </RelativeLayout>

在Activity中的基本使用

public class CardFoldActivity extends BaseActivity { @BindView(R.id.fold_view) FoldingLayout foldView; @BindView(R.id.btnFold) Button btnFold; @BindView(R.id.btnOpen) Button btnOpen; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.act_card_fold); ButterKnife.bind(this); foldView.setParams(true, 0.5f, true, 0.5f, 0.3f); foldView.setOrientation(Orientation.VERTICAL); btnFold.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { foldView.fold(); } }); btnOpen.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { foldView.open(); } }); } }

折叠控件FoldingLayout的全部代码(有点长,不想看的话可以直接复制使用):

public class FoldingLayout extends ViewGroup { public interface OnFoldListener { public void onStartFold(); public void onEndFold(); } public static enum Orientation { VERTICAL, HORIZONTAL } private final String FOLDING_VIEW_EXCEPTION_MESSAGE = "Folding Layout can only 1 child at most"; private final float SHADING_ALPHA = 0.8f; private final float SHADING_FACTOR = 0.5f; private final int DEPTH_CONSTANT = 1500; private final int NUM_OF_POLY_POINTS = 8; private Rect[] mFoldRectArray; private Matrix [] mMatrix; private Orientation mOrientation = Orientation.HORIZONTAL; private float mAnchorFactor = 0; private float mFoldFactor = 0; private int mNumberOfFolds = 2; private boolean mIsHorizontal = true; private int mOriginalWidth = 0; private int mOriginalHeight = 0; private float mFoldMaxWidth = 0; private float mFoldMaxHeight = 0; private float mFoldDrawWidth = 0; private float mFoldDrawHeight = 0; private boolean mIsFoldPrepared = false; private boolean mShouldDraw = true; private Paint mSolidShadow; private Paint mGradientShadow; private LinearGradient mShadowLinearGradient; private Matrix mShadowGradientMatrix; private float [] mSrc; private float [] mDst; private OnFoldListener mFoldListener; private float mPreviousFoldFactor = 0; private Bitmap mFullBitmap; private Rect mDstRect; public FoldingLayout(Context context) { super(context); } public FoldingLayout(Context context, AttributeSet attrs) { super(context, attrs); } public FoldingLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } /********************** 常用API ******************************/ private boolean scale = true; private float scaleNum = 0.5f; private boolean alpha = true; private float alphaEnd = 1; private float foldEnd = 1; /** * * @param scale 是否开启缩放 * @param scaleNum 折叠后最终缩放比例 0~1 * @param alpha 是否开启渐隐渐显 * @param alphaEnd 折叠后最终透明度 0:完全透明 1:完全不透明 * @param foldEnd 折叠后最终幅度 0~1 1:完全折叠 */ public void setParams(boolean scale, float scaleNum, boolean alpha, float alphaEnd, float foldEnd){ this.scale = scale; this.scaleNum = scaleNum; this.alpha = alpha; this.alphaEnd = alphaEnd; this.foldEnd = foldEnd; } /** * 折叠 */ public void fold(){ ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1); valueAnimator.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float value = (float) animation.getAnimatedValue(); if (scale){ float scale = 1 - value*(1 - scaleNum); setScaleX(scale); setScaleY(scale); } if (alpha){ setAlpha(1 - value * (1 - alphaEnd)); } setFoldFactor(value * foldEnd); } }); valueAnimator.setDuration(500); valueAnimator.start(); } /** * 展开 */ public void open(){ ValueAnimator valueAnimator = ValueAnimator.ofFloat(1, 0); valueAnimator.addUpdateListener(new AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { setVisibility(VISIBLE); float value = (float) animation.getAnimatedValue(); if (scale){ float scale = 1 - value*(1 - scaleNum); setScaleX(scale); setScaleY(scale); } if (alpha){ setAlpha(1 - value * (1 - alphaEnd)); } setFoldFactor(value * foldEnd); } }); valueAnimator.setDuration(500); valueAnimator.start(); } /********************** 基本API ******************************/ public void setFoldListener(OnFoldListener foldListener) { mFoldListener = foldListener; } /** * Sets the fold factor of the folding view and updates all the corresponding * matrices and values to account for the new fold factor. Once that is complete, * it redraws itself with the new fold. */ public void setFoldFactor(float foldFactor) { if (foldFactor != mFoldFactor) { mFoldFactor = foldFactor; calculateMatrices(); invalidate(); } } public void setOrientation(Orientation orientation) { if (orientation != mOrientation) { mOrientation = orientation; updateFold(); } } public void setAnchorFactor(float anchorFactor) { if (anchorFactor != mAnchorFactor) { mAnchorFactor = anchorFactor; updateFold(); } } public void setNumberOfFolds(int numberOfFolds) { if (numberOfFolds != mNumberOfFolds) { mNumberOfFolds = numberOfFolds; updateFold(); } } public float getAnchorFactor() { return mAnchorFactor; } public Orientation getOrientation() { return mOrientation; } public float getFoldFactor() { return mFoldFactor; } public int getNumberOfFolds() { return mNumberOfFolds; } /********************** 私有方法 ******************************/ @Override public void addView(View child, int index, LayoutParams params) { throwCustomException(getChildCount()); super.addView(child, index, params); } @Override protected boolean addViewInLayout(View child, int index, LayoutParams params, boolean preventRequestLayout) { throwCustomException(getChildCount()); boolean returnValue = super.addViewInLayout(child, index, params, preventRequestLayout); return returnValue; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { View child = getChildAt(0); measureChild(child,widthMeasureSpec, heightMeasureSpec); setMeasuredDimension(widthMeasureSpec, heightMeasureSpec); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { View child = getChildAt(0); child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight()); updateFold(); setVisibility(INVISIBLE); } /** * The custom exception to be thrown so as to limit the number of views in this * layout to at most one. */ private class NumberOfFoldingLayoutChildrenException extends RuntimeException { public NumberOfFoldingLayoutChildrenException(String message) { super(message); } } /** Throws an exception if the number of views added to this layout exceeds one.*/ private void throwCustomException (int numOfChildViews) { if (numOfChildViews == 1) { throw new NumberOfFoldingLayoutChildrenException(FOLDING_VIEW_EXCEPTION_MESSAGE); } } private void updateFold() { prepareFold(mOrientation, mAnchorFactor, mNumberOfFolds); calculateMatrices(); invalidate(); } /** * This method is called in order to update the fold's orientation, anchor * point and number of folds. This creates the necessary setup in order to * prepare the layout for a fold with the specified parameters. Some of the * dimensions required for the folding transformation are also acquired here. * * After this method is called, it will be in a completely unfolded state by default. */ private void prepareFold(Orientation orientation, float anchorFactor, int numberOfFolds) { mSrc = new float[NUM_OF_POLY_POINTS]; mDst = new float[NUM_OF_POLY_POINTS]; mDstRect = new Rect(); mFoldFactor = 0; mPreviousFoldFactor = 0; mIsFoldPrepared = false; mSolidShadow = new Paint(); mGradientShadow = new Paint(); mOrientation = orientation; mIsHorizontal = (orientation == Orientation.HORIZONTAL); if (mIsHorizontal) { mShadowLinearGradient = new LinearGradient(0, 0, SHADING_FACTOR, 0, Color.BLACK, Color.TRANSPARENT, TileMode.CLAMP); } else { mShadowLinearGradient = new LinearGradient(0, 0, 0, SHADING_FACTOR, Color.BLACK, Color.TRANSPARENT, TileMode.CLAMP); } mGradientShadow.setStyle(Style.FILL); mGradientShadow.setShader(mShadowLinearGradient); mShadowGradientMatrix = new Matrix(); mAnchorFactor = anchorFactor; mNumberOfFolds = numberOfFolds; mOriginalWidth = getMeasuredWidth(); mOriginalHeight = getMeasuredHeight(); mFoldRectArray = new Rect[mNumberOfFolds]; mMatrix = new Matrix [mNumberOfFolds]; for (int x = 0; x < mNumberOfFolds; x++) { mMatrix[x] = new Matrix(); } int h = mOriginalHeight; int w = mOriginalWidth; if (FoldingLayoutActivity.IS_JBMR2) { mFullBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(mFullBitmap); getChildAt(0).draw(canvas); } int delta = Math.round(mIsHorizontal ? ((float) w) / ((float) mNumberOfFolds) : ((float) h) /((float) mNumberOfFolds)); /* Loops through the number of folds and segments the full layout into a number * of smaller equal components. If the number of folds is odd, then one of the * components will be smaller than all the rest. Note that deltap below handles * the calculation for an odd number of folds.*/ for (int x = 0; x < mNumberOfFolds; x++) { if (mIsHorizontal) { int deltap = (x + 1) * delta > w ? w - x * delta : delta; mFoldRectArray[x] = new Rect(x * delta, 0, x * delta + deltap, h); } else { int deltap = (x + 1) * delta > h ? h - x * delta : delta; mFoldRectArray[x] = new Rect(0, x * delta, w, x * delta + deltap); } } if (mIsHorizontal) { mFoldMaxHeight = h; mFoldMaxWidth = delta; } else { mFoldMaxHeight = delta; mFoldMaxWidth = w; } mIsFoldPrepared = true; } /* * Calculates the transformation matrices used to draw each of the separate folding * segments from this view. */ private void calculateMatrices() { mShouldDraw = true; if (!mIsFoldPrepared) { return; } /** If the fold factor is 1 than the folding view should not be seen * and the canvas can be left completely empty. */ if (mFoldFactor == 1) { mShouldDraw = false; return; } if (mFoldFactor == 0 && mPreviousFoldFactor > 0) { if(mFoldListener != null){ mFoldListener.onEndFold(); } } if (mPreviousFoldFactor == 0 && mFoldFactor > 0) { if (mFoldListener!=null) { mFoldListener.onStartFold(); } } mPreviousFoldFactor = mFoldFactor; /* Reset all the transformation matrices back to identity before computing * the new transformation */ for (int x = 0; x < mNumberOfFolds; x++) { mMatrix[x].reset(); } float cTranslationFactor = 1 - mFoldFactor; float translatedDistance = mIsHorizontal ? mOriginalWidth * cTranslationFactor : mOriginalHeight * cTranslationFactor; float translatedDistancePerFold = Math.round(translatedDistance / mNumberOfFolds); /* For an odd number of folds, the rounding error may cause the * translatedDistancePerFold to be grater than the max fold width or height. */ mFoldDrawWidth = mFoldMaxWidth < translatedDistancePerFold ? translatedDistancePerFold : mFoldMaxWidth; mFoldDrawHeight = mFoldMaxHeight < translatedDistancePerFold ? translatedDistancePerFold : mFoldMaxHeight; float translatedDistanceFoldSquared = translatedDistancePerFold * translatedDistancePerFold; /* Calculate the depth of the fold into the screen using pythagorean theorem. */ float depth = mIsHorizontal ? (float)Math.sqrt((double)(mFoldDrawWidth * mFoldDrawWidth - translatedDistanceFoldSquared)) : (float)Math.sqrt((double)(mFoldDrawHeight * mFoldDrawHeight - translatedDistanceFoldSquared)); /* The size of some object is always inversely proportional to the distance * it is away from the viewpoint. The constant can be varied to to affect the * amount of perspective. */ float scaleFactor = DEPTH_CONSTANT / (DEPTH_CONSTANT + depth); float scaledWidth, scaledHeight, bottomScaledPoint, topScaledPoint, rightScaledPoint, leftScaledPoint; if (mIsHorizontal) { scaledWidth = mFoldDrawWidth * cTranslationFactor; scaledHeight = mFoldDrawHeight * scaleFactor; } else { scaledWidth = mFoldDrawWidth * scaleFactor; scaledHeight = mFoldDrawHeight * cTranslationFactor; } topScaledPoint = (mFoldDrawHeight - scaledHeight) / 2.0f; bottomScaledPoint = topScaledPoint + scaledHeight; leftScaledPoint = (mFoldDrawWidth - scaledWidth) / 2.0f; rightScaledPoint = leftScaledPoint + scaledWidth; float anchorPoint = mIsHorizontal ? mAnchorFactor * mOriginalWidth : mAnchorFactor * mOriginalHeight; /* The fold along which the anchor point is located. */ float midFold = mIsHorizontal ? (anchorPoint / mFoldDrawWidth) : anchorPoint / mFoldDrawHeight; mSrc[0] = 0; mSrc[1] = 0; mSrc[2] = 0; mSrc[3] = mFoldDrawHeight; mSrc[4] = mFoldDrawWidth; mSrc[5] = 0; mSrc[6] = mFoldDrawWidth; mSrc[7] = mFoldDrawHeight; /* Computes the transformation matrix for each fold using the values calculated above. */ for (int x = 0; x < mNumberOfFolds; x++) { boolean isEven = (x % 2 == 0); if (mIsHorizontal) { mDst[0] = (anchorPoint > x * mFoldDrawWidth) ? anchorPoint + (x - midFold) * scaledWidth : anchorPoint - (midFold - x) * scaledWidth; mDst[1] = isEven ? 0 : topScaledPoint; mDst[2] = mDst[0]; mDst[3] = isEven ? mFoldDrawHeight: bottomScaledPoint; mDst[4] = (anchorPoint > (x + 1) * mFoldDrawWidth) ? anchorPoint + (x + 1 - midFold) * scaledWidth : anchorPoint - (midFold - x - 1) * scaledWidth; mDst[5] = isEven ? topScaledPoint : 0; mDst[6] = mDst[4]; mDst[7] = isEven ? bottomScaledPoint : mFoldDrawHeight; } else { mDst[0] = isEven ? 0 : leftScaledPoint; mDst[1] = (anchorPoint > x * mFoldDrawHeight) ? anchorPoint + (x - midFold) * scaledHeight : anchorPoint - (midFold - x) * scaledHeight; mDst[2] = isEven ? leftScaledPoint: 0; mDst[3] = (anchorPoint > (x + 1) * mFoldDrawHeight) ? anchorPoint + (x + 1 - midFold) * scaledHeight : anchorPoint - (midFold - x - 1) * scaledHeight; mDst[4] = isEven ? mFoldDrawWidth : rightScaledPoint; mDst[5] = mDst[1]; mDst[6] = isEven ? rightScaledPoint : mFoldDrawWidth; mDst[7] = mDst[3]; } /* Pixel fractions are present for odd number of folds which need to be * rounded off here.*/ for (int y = 0; y < 8; y ++) { mDst[y] = Math.round(mDst[y]); } /* If it so happens that any of the folds have reached a point where * the width or height of that fold is 0, then nothing needs to be * drawn onto the canvas because the view is essentially completely * folded.*/ if (mIsHorizontal) { if (mDst[4] <= mDst[0] || mDst[6] <= mDst[2]) { mShouldDraw = false; return; } } else { if (mDst[3] <= mDst[1] || mDst[7] <= mDst[5]) { mShouldDraw = false; return; } } /* Sets the shadow and bitmap transformation matrices.*/ mMatrix[x].setPolyToPoly(mSrc, 0, mDst, 0, NUM_OF_POLY_POINTS / 2); } /* The shadows on the folds are split into two parts: Solid shadows and gradients. * Every other fold has a solid shadow which overlays the whole fold. Similarly, * the folds in between these alternating folds also have an overlaying shadow. * However, it is a gradient that takes up part of the fold as opposed to a solid * shadow overlaying the whole fold.*/ /* Solid shadow paint object. */ int alpha = (int) (mFoldFactor * 255 * SHADING_ALPHA); mSolidShadow.setColor(Color.argb(alpha, 0, 0, 0)); if (mIsHorizontal) { mShadowGradientMatrix.setScale(mFoldDrawWidth, 1); mShadowLinearGradient.setLocalMatrix(mShadowGradientMatrix); } else { mShadowGradientMatrix.setScale(1, mFoldDrawHeight); mShadowLinearGradient.setLocalMatrix(mShadowGradientMatrix); } mGradientShadow.setShader(mShadowLinearGradient); mGradientShadow.setAlpha(alpha); } @Override protected void dispatchDraw(Canvas canvas) { /** If prepareFold has not been called or if preparation has not completed yet, * then no custom drawing will take place so only need to invoke super's * onDraw and return. */ if (!mIsFoldPrepared || mFoldFactor == 0) { super.dispatchDraw(canvas); return; } if (!mShouldDraw) { return; } Rect src; /* Draws the bitmaps and shadows on the canvas with the appropriate transformations. */ for (int x = 0; x < mNumberOfFolds; x++) { src = mFoldRectArray[x]; /* The canvas is saved and restored for every individual fold*/ canvas.save(); /* Concatenates the canvas with the transformation matrix for the * the segment of the view corresponding to the actual image being * displayed. */ canvas.concat(mMatrix[x]); if (FoldingLayoutActivity.IS_JBMR2) { mDstRect.set(0, 0, src.width(), src.height()); canvas.drawBitmap(mFullBitmap, src, mDstRect, null); } else { /* The same transformation matrix is used for both the shadow and the image * segment. The canvas is clipped to account for the size of each fold and * is translated so they are drawn in the right place. The shadow is then drawn on * top of the different folds using the sametransformation matrix.*/ canvas.clipRect(0, 0, src.right - src.left, src.bottom - src.top); if (mIsHorizontal) { canvas.translate(-src.left, 0); } else { canvas.translate(0, -src.top); } super.dispatchDraw(canvas); if (mIsHorizontal) { canvas.translate(src.left, 0); } else { canvas.translate(0, src.top); } } /* Draws the shadows corresponding to this specific fold. */ if (x % 2 == 0) { canvas.drawRect(0, 0, mFoldDrawWidth, mFoldDrawHeight, mSolidShadow); } else { canvas.drawRect(0, 0, mFoldDrawWidth, mFoldDrawHeight, mGradientShadow); } canvas.restore(); } } }
最新回复(0)