preface

This article is based on the AnimationListView framework, which was explained in detail in the previous article. It is recommended that you familiarize yourself with the Android Magic Series: Step by Step To Achieve a Page in Half.

In the previous chapter we implemented the folding effect and also implemented a framework for AnimationListView, which allows us to implement many effects.

In this article, we will implement a louver effect under this framework. The effect is as follows:

Implement the AnimationViewInterface interface

If you want to apply an effect to an AnimationListView, you need to implement the AnimationViewInterface interface as follows

public class BlindsView extends LinearLayout implements AnimationViewInterface{
Copy the code

BlindsView inherits a LinearLayout.

Parsing animation composition

Let’s take a look at one of these frames, as follows

It can be seen that the whole shutter effect is actually composed of small squares. These squares flip horizontally and have a time difference in different columns, thus forming the shutter effect.

So BlindsView actually contains many of these child views. The real animation is generated by the rollover of these child views, so BlindsView inherits this LinearLayout.

Flip unit — RotateView

The child view mentioned above, which we define as RotateView, inherits ImageView in order to load the image. As follows:

public class RotateView extends ImageView {
Copy the code

Foreground background

Look at the square shown in the figure below and compare it with another square in the same position.

You can see that when flipped 180 degrees, the square shows another image, which is actually the same part of the next page. So each RotateView needs two images, foreground and background, as follows:

public void setBitmap(Bitmap frontBitmap, Bitmap backBitmap){
    if(frontBitmap == null) {return;
    }
    mFrontBitmap = frontBitmap;
    mBackBitmap = backBitmap;
    mRotatedBackBitmap = null;
    setImageBitmap(frontBitmap);
    setScaleType(ScaleType.FIT_XY);
    // Initialize the rollover Angle
    setRotationX(0);
    setRotationY(0);
}
Copy the code

The code is relatively simple, the default display foreground.

There’s an mRotateBackBitmap, which we’ll talk about later.

tips

The code is as follows:

public void setRotation(float value, boolean isRotateX){
    // Set the flip Angle
    if(isRotateX){
        setRotationX(value);
    }
    else {
        setRotationY(value);
    }
    // Change the Angle between 0 and 360 for later judgment
    float rotate = value % 360;
    if (rotate < 0) {
        rotate += 360;
    }
    /** * Set zoom: when the vertical flip to shrink, conversely, the main reason to restore the * zoom is in the flip, the image will be deformed into a trapezoid, then the image center axis maintain the original width, * then flip up the side will become larger, part of the image will be beyond the display. So I'm going to scale it, * and the size of the scale varies according to the actual needs. * /
    float scale = rotate > 180 ? Math.abs(rotate - 270) : Math.abs(rotate - 90);
    scale = scale / 90 * (1 - mScaleMin) + mScaleMin;
    if(isRotateX){
        setScaleX(scale);
    }
    else{
        setScaleY(scale);
    }
 
    // Set the foreground/background image according to the position of the flip
    if(mBackBitmap ! =null) {
        if(mRotatedBackBitmap == null || this.isRotateX ! = isRotateX) {/** * The background image will first be flipped according to the direction of the flip * so that the background image will not be flipped left and right */
            Matrix matrix = new Matrix();
            if (isRotateX) {
                matrix.postScale(1, -1);
            } else {
                matrix.postScale(-1.1);
            }
            mRotatedBackBitmap = Bitmap.createBitmap(mBackBitmap, 0.0,
                    mBackBitmap.getWidth(), mBackBitmap.getHeight(), matrix, true);
        }
        /** * When flipped, the background image is displayed in quadrants 2 and 3, and the foreground image is displayed in quadrants 1 and 4
        if (rotate > 90 && rotate < 270) {
            setImageBitmap(mRotatedBackBitmap);
        } else{ setImageBitmap(mFrontBitmap); }}this.isRotateX = isRotateX;
}
Copy the code

Two parameters, the first parameter is the Angle of the flip, and the second parameter is the flip direction (horizontal or vertical).

The flip is simple, just call setRotationX or setRotationY function, mainly the foreground and background image switch.

Pay attention to the second part of the code. The reason why scaling is done here is that the side on the outside of the flip side will increase and exceed the area due to the effect of near-large and far-small during the flip, which is not good for the visual effect. Therefore, scaling is done to ensure that the whole flipping process can be completely presented in the area. You can try to get rid of this code and compare it, but I won’t show it here.

The last step of the code is to set up different images according to the Angle of reversal. Focus on the background image, since the background image should actually be mirrored horizontally, flip it horizontally in advance to produce the mRotateBackBitmap. To prevent the reverse operation from being performed every time, determine that if there is an mRotateBackBitmap and the reverse direction does not change, you do not need to perform the reverse operation. So if you change the background, reset mRotateBackBitmap to NULL, as mentioned in the setBitmap function above.

So when we call the setRotate method and set different angles we get different rollover effects.

Implement flip animation

For RotateView, all you really need is setRotate. The animation part is handled in BlindsView and setRotate is called. But we also wanted this class to stand alone, so I added its own animation handling, as follows:

public void rotateXAnimation(float fromRotate, float toRotate, long duration){
    rotateAnimation(fromRotate, toRotate, duration, 0.true);
}
 
/** * flip animation *@paramFromRotate Start Angle *@paramToRotate End Angle *@param duration
 * @paramDelay Animation delay *@paramIsRotateX is centered on X */
private void rotateAnimation(float fromRotate, float toRotate, long duration, long delay, boolean isRotateX){
    if(mAnimator ! =null){
        mAnimator.cancel();
    }
    mAnimator = ValueAnimator.ofFloat(fromRotate, toRotate);
    mAnimator.setStartDelay(delay);
    mAnimator.setDuration(duration);
    mAnimator.start();
    mAnimator.addUpdateListener(new RotateListener(isRotateX));
}
 
class RotateListener implements ValueAnimator.AnimatorUpdateListener{
    private boolean isRotateX;
 
    public RotateListener(boolean isRotateX){
        this.isRotateX = isRotateX;
    }
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        floatvalue = (Float)(animation.getAnimatedValue()); setRotation(value, isRotateX); }}Copy the code

In fact, it is also very simple, with the property animation to achieve. I’m just going to use ValueAnimator so that the value of the animation gradually changes from fromRotate to toRotate. The flipped animation is achieved by setting a listener for the animation and calling the setRotate function.

The shutters – BlindsView

Above we have completed the rollover unit RotateView. Now we will explain how to use these units to compose the louver effect.

Initialize the image matrix

Cut the whole foreground and background images into small images and set them to RotateView and matrix them into BlindsView as follows:

public void setBitmap(Bitmap frontBitmap, Bitmap backBitmap){
    // Process the image
    List<Bitmap> subFrontBitmaps = getSubBitmaps(mRowCount, mColumnCount, frontBitmap);
    List<Bitmap> subBackBitmaps = getSubBitmaps(mRowCount, mColumnCount, backBitmap);
    setBitmaps(mRowCount, mColumnCount, subFrontBitmaps, subBackBitmaps);
}
 
/** * Get image array * split large image into rowCount*columnCount array small image *@param rowCount
 * @param columnCount
 * @param bitmap
 * @return* /
private List<Bitmap> getSubBitmaps(int rowCount, int columnCount, Bitmap bitmap){
    List<Bitmap> subBitmaps = new ArrayList<Bitmap>();
    int subWidth = bitmap.getWidth() / columnCount;
    int subHeight = bitmap.getHeight() / rowCount;
    for(int i = 0; i < rowCount; i++){
        for(int j = 0; j < columnCount; j++){
            /** * the size of each leaf image is calculated */
            int height = i == rowCount - 1 ? bitmap.getHeight() - subHeight * i : subHeight;
            int width = j == columnCount - 1? bitmap.getWidth() - subWidth * j : subWidth; Bitmap subBitmap = Bitmap.createBitmap(bitmap, subWidth * j, subHeight * i, width, height); subBitmaps.add(subBitmap); }}return subBitmaps;
}
 
/** * Set the image array * put the array of foreground and background images into each RotateView *@param rowCount
 * @param columnCount
 * @param mFrontBitmaps
 * @param mBackBitmaps
 */
private void setBitmaps(int rowCount, int columnCount, List<Bitmap> mFrontBitmaps, List<Bitmap> mBackBitmaps){
    / * * * in order to reuse, dealing with * first needs to be done to determine whether existing row/column redundant, directly remove excess, insufficient supplement * /
    // The maximum number of existing rows and the maximum number of target rows.
    int maxRow = Math.max(getChildCount() , rowCount);
    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
            LinearLayout.LayoutParams.MATCH_PARENT, 1);
    params.weight = 1;
    for(int i = 0; i < maxRow; i++){
        LinearLayout subView = null;
        if(i >= getChildCount() && i < rowCount){
            // If the number of existing rows is insufficient, it will be added. Each row is a horizontal linearlayout
            subView = new LinearLayout(getContext());
            subView.setOrientation(HORIZONTAL);
            addView(subView, params);
        }
        else if(i < getChildCount() && i >= rowCount){
            // If there are too many existing rows, remove them
            removeViewAt(i);
            i--;
            maxRow--;
        }
        else{
            subView = (LinearLayout)getChildAt(i);
        }
        // Start processing each item in each row
        if(subView ! =null) {// The maximum number of columns is the maximum number of existing columns and the target columns.
            int maxColumn = Math.max(subView.getChildCount() , columnCount);
            LinearLayout.LayoutParams subParams = new LinearLayout.LayoutParams(
                    1, LinearLayout.LayoutParams.MATCH_PARENT);
            subParams.weight = 1;
            for(int j = 0; j < maxColumn; j++){
                RotateView rotateView = null;
                if(j >= columnCount && j < subView.getChildCount()){
                    // If there are too many existing columns, remove them
                    subView.removeViewAt(j);
                    j--;
                    maxColumn--;
                }
                else if(j < columnCount && j >= subView.getChildCount()){
                    // If the existing column is insufficient, it will be added. Each leaf is RotateView
                    rotateView = new RotateView(getContext());
                    subView.addView(rotateView, subParams);
                }
                else{
                    rotateView = (RotateView)subView.getChildAt(j);
                }
                // Fill the image for the rearranged matrix
                if(rotateView ! =null) {int index = i * columnCount + j;
                    rotateView.setBitmap(mFrontBitmaps.get(index), mBackBitmaps.get(index));
                    rotateView.setScaleMin(0.5 f);
                }
            }
        }
    }
}
Copy the code

As you can see, the getSubBitmaps function is called to split the foreground and background images and return a list.

The setBitmaps function is then called to create a new RotateView based on the specified rows and columns, passing in the corresponding image and adding it to the layout.

Note that the existing RotateView will be reused. If it is insufficient, it will be replenished and the excess will be removed.

This is a lot of code, but it’s essentially adding a horizontal LinearLayout per line (BlindsView itself is vertical) and then adding or reusing RotateView line by line and setBitmap it.

Manual flip shutter

As with the folding effect in the previous article, the movement of the shutter effect includes manual and automatic parts. When the user touches the screen and moves, the shutter moves with the move event of the touch. When the user touches up or end, an animation automatically completes the rest of the animation.

The setAnimationPrecent method of AnimationViewInterface should be implemented in the manual movement phase as follows:

@Override
public void setAnimationPercent(float percent, MotionEvent event, boolean isVertical){
    mAnimationPercent = percent;
    // Get the total rotation Angle
    float value = mAnimationPercent * getTotalVaule(isVertical);
    /** * Sets the current Angle of each facet to be traversed */ depending on the direction of rotation, starting from different positions */
    for(int i = 0; i < mRowCount; i++){
        LinearLayout parent = (LinearLayout)getChildAt(i);
        for(int j = 0; j < mColumnCount; j++){
            RotateView view = (RotateView)parent.getChildAt(j);
            float subValue;
            if(value > 0) {if(isVertical){
                    // Slide down. Start with the first row and rotate at a decreasing Angle
                    subValue = value - mSpace * i;
                }
                else{
                    // Swipe right. Rotate from the first column, decreasing the Angle of rotation in each column
                    subValue = value - mSpace * j;
                }
                // Keep the rotation Angle between 0 and 180 degrees
                if(subValue < 0){
                    subValue = 0;
                }
                else if(subValue > 180){
                    subValue = 180; }}else{
                if(isVertical){
                    // Slide down. Rotate from the last row, decreasing the rotation Angle for each row.
                    subValue = value + mSpace * (mRowCount - i - 1);
                }
                else{
                    // Swipe left. Rotate from the last column, decreasing the Angle of rotation in each column.
                    subValue = value + mSpace * (mColumnCount - j - 1);
                }
                // Keep the rotation Angle between 0 and -180 degrees
                if(subValue < -180){
                    subValue = -180;
                }
                else if(subValue > 0){
                    subValue = 0; }}// Note that if you roll up and down, the Angle needs to be negative, otherwise the rotation direction is wrongview.setRotation(isVertical ? -subValue : subValue, isVertical); }}}Copy the code

As you can see, we started by using getTotalValue to calculate the total rotation Angle as follows:

private float getTotalVaule(boolean isVertical){
    if(isVertical) {
        return mSpace * (mRowCount - 1) + 180;
    }
    else{
        return mSpace * (mColumnCount - 1) + 180; }}Copy the code

This one needs some explanation. As can be seen from the above picture, the rotation Angle of each column is different, and there will be a difference of Angle between adjacent columns, namely mSpace.

So what does the getTotalValue function evaluate?

In a full flip, when the first column is flipped, the other columns are not, so the process is not over.

So let’s say the first column continues to flip, and by the time the second column has flipped, the first column has flipped mSpace times 1 plus 180. ColumnCount – 1 = mSpace * (columnCount – 1) + 180

So mAnimationPercent * getTotalVaule(isVertical) is actually the current flip Angle of the first column, so you can calculate the flip Angle of the other columns. Just set the rotation for each RotateView.

But note that this is not a true flip Angle, as it is no longer needed after it has been completely flipped 180 degrees.

The code handles flips in four directions, so the calculation is somewhat different, and the idea is the same.

Automatic flip shutter

In the automatic phase, startAnimation function is implemented, and the code is as follows:

@Override
public void startAnimation(boolean isVertical, MotionEvent event, float toPercent){
    if(mAnimator ! =null && mAnimator.isRunning()){
        return;
    }
    mAnimator = ValueAnimator.ofFloat(mAnimationPercent, toPercent);
    // The duration of the animation varies depending on the starting position
    mAnimator.setDuration((long) (Math.abs(toPercent - mAnimationPercent) * mDuration));
    mAnimator.start();
    OnAnimationListener onAnimationListener = new OnAnimationListener(isVertical, toPercent);
    mAnimator.addUpdateListener(onAnimationListener);
    mAnimator.addListener(onAnimationListener);
}
 
class OnAnimationListener implements ValueAnimator.AnimatorUpdateListener.Animator.AnimatorListener{
    private boolean isVertical;
    private float toPercent;
    public OnAnimationListener(boolean isVertical, float toPercent){
        this.isVertical = isVertical;
        this.toPercent = toPercent;
    }
    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        setAnimationPercent((float)animation.getAnimatedValue(), null, isVertical);
    }
 
    @Override
    public void onAnimationStart(Animator animation) {}@Override
    public void onAnimationEnd(Animator animation) {
        mAnimationPercent = 0;
        if(mOnAnimationViewListener == null) {return;
        }
        if(toPercent == 1){
            mOnAnimationViewListener.pagePrevious();
        }
        else if(toPercent == -1){ mOnAnimationViewListener.pageNext(); }}@Override
    public void onAnimationCancel(Animator animation) {
        mAnimationPercent = 0;
    }
 
    @Override
    public void onAnimationRepeat(Animator animation) {}}Copy the code

This is done by listening to the property animation of a float and then changing the flip state with setAnimationPrecent.

Note that the page-cutting callback is called at the end of the animation.

This part is similar to the folding effect of the previous article, so I won’t go into details.

To summarize

From these two articles, you should have an understanding of the AnimationListView framework. There are many more cool effects that can be implemented with this framework, and the code can generally reference these two effects. On this framework and implementation we will stop for the time being, next we will analyze some other, later we have the opportunity to achieve more effects on this framework, if you have any good ideas or their implementation of the effect can leave a message.

Pay attention to the public number: BennuCTech, get more dry goods!