应项目需求,参考网络资源并整理成适合当前项目所用,为防止二次重复整理,特在此记录一笔。
这里主要提供的是一个可折叠的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();
}
}
}