卡片折叠效果

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

这里主要提供的是一个可折叠的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();
    }
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值