preface

This is a long time ago an article, recently someone mentioned to remember, just here has not been published, rearrange a distribution. This series has been on hold for a long time. Let’s see if we can get it started again.

This article is mainly to achieve a folding effect, mainly to learn Android screen capture, Bitmap processing and canvas drawing these knowledge.

The result is as follows

AnimationListView framework

Cause of frame

Some common interfaces and classes are sorted out because several effects are treated in a similar way, which can be regarded as a series. This article will introduce them in detail, so the length will be longer and may be a bit boring.

First of all, we’re not just going to do a fold effect, we’re actually going to view the whole thing as a special ViewPager, where each Item fills the screen, and we’re going to switch items in half. A closer example of life would be a wall calendar, page by page, turning up and down.

So the fold effect is the transition effect for the switch, and we’re going to implement this ViewPager — AnimationListView first, and then add the effect.

AnimationListView this class code is more, here will not post the whole, you can go to the project source code to view, here will only be the key part of the code explained.

Implementing page caching

AnimationListView is similar in many ways to ViewPager. It uses Adapter to load each page and caches three pages: the current page, the previous page, and the next page. This part of the code is as follows:

/** * Set the adapter, set the listener and rearrange the page *@param adapter
 */
public void setAdapter(Adapter adapter) {
    mAdapter = adapter;
    mAdapter.registerDataSetObserver(new DataSetObserver() {
        @Override
        public void onChanged(a) {
            super.onChanged();
            refreshByAdapter();
        }
 
        @Override
        public void onInvalidated(a) {
            super.onInvalidated(); refreshByAdapter(); }}); mCurrentPosition =0;
    refreshByAdapter();
}
 
/** * Rearrange the page * add mCacheItems before adding mFolioView. So that the mFolioView is always at the top and not blocked. * /
private void refreshByAdapter(a) {
    removeAllViews();
    if (mCurrentPosition < 0) {
        mCurrentPosition = 0;
    }
    if (mCurrentPosition >= mAdapter.getCount()) {
        mCurrentPosition = mAdapter.getCount() - 1;
    }
    // If there are not enough items in the cache, add the first item to the cache
    while(mCacheItems.size() < 3){
        View item = mAdapter.getView(0.null.null);
        addView(item, mLayoutParams);
        mCacheItems.add(item);
    }
    // Refresh the cached item data.
    for (int i = 0; i < mCacheItems.size(); i++) {
        int index = mCurrentPosition + i - 1;
        View item = mCacheItems.get(i);
        // When at the top or bottom of the list, there is a cache Item that is not refreshed because there is no previous or next position in the current position
        if (index >= 0 && index < mAdapter.getCount()) {
            item = mAdapter.getView(index, item, null); }}// Refresh the interface
    initItemVisible();
    // Add a flipped view
    setAnimationViewVisible(false);
}
 
/** ** next page */
protected void pageNext(a) {
    setAnimationViewVisible(false);
    // Add 1 to the current position
    mCurrentPosition++;
    if (mCurrentPosition >= mAdapter.getCount()) {
        mCurrentPosition = mAdapter.getCount() - 1;
    }
    // Remove the first item from the cache, refresh it to the next bit in its current position, and add it to the bottom of the cache list
    View first = mCacheItems.remove(0);
    if (mCurrentPosition + 1 < mAdapter.getCount()) {
        first = mAdapter.getView(mCurrentPosition + 1, first, null);
    }
    mCacheItems.add(first);
    // Refresh the interface
    initItemVisible();
}
 
/** ** ** /
protected void pagePrevious(a) {
    // Current position minus 1
    mCurrentPosition--;
    if (mCurrentPosition < 0) {
        mCurrentPosition = 0;
    }
    // Remove the last item from the cache, refresh it to a bit above its current position, and add it to the start of the cache list
    View last = mCacheItems.remove(mCacheItems.size() - 1);
    if (mCurrentPosition - 1> =0) {
        last = mAdapter.getView(mCurrentPosition - 1, last, null);
    }
    mCacheItems.add(0, last);
    // Refresh the interface
    initItemVisible();
    setAnimationViewVisible(false);
}
 
 
/** * Flushes all items and displays only the middle item */ in its current position
private void initItemVisible(a) {
    for (int i = 0; i < mCacheItems.size(); i++) {
        View item = mCacheItems.get(i);
        item.invalidate();
        if (item == null) {
            continue;
        }
        if (i == 1) {
            item.setVisibility(VISIBLE);
        } else{ item.setVisibility(INVISIBLE); }}}Copy the code

First, the function refreshByAdpter is called when the Adapter data changes. It initializes the page to make the Adapter valid based on the current position.

In this function, three (or two, when at the beginning or end) views are retrieved from the Adapter based on the current position and cached, and all three cached views are added to the page. The reason for adding all three views to the page, rather than just the current page, is because of the need to implement the switch effect, which will be explained later.

When all three views are added to the page, you can see that the initItemVisible function is called again. As you can see from the code, this function mainly handles the presentation of the three Views. Making the current page VISIBLE and the other pages INVISIBLE ensures that the current page is VISIBLE.

Finally, the setAnimationViewVisible function is called, which is used to show the view that hides the toggle animation, as discussed later.

Then, the methods pageNext and pagePrevious are similar in that they cut the page up and down (without the switching animation), respectively. In the case of pageNext, take the first view that cached mCacheItems, reload the next page of data for that view, then add it back to the mCacheItems tail, and call initItemVisible to reset the display. This displays the next page and also caches the next page.

Handling touch events

Ok, let’s look at a toggle operation. Since this switch is not just an animation, the entire effect actually changes with a finger swipe, so you need to handle the touch event as follows:

@Override
public boolean onTouchEvent(MotionEvent event) {
    if (getWidth() <= 0 || getHeight() <= 0) {
        return false;
    }
    // The touch event is ignored while the animation component is animating
    if(mAnimationView ! =null && mAnimationView.isAnimationRunning()){
        return true;
    }
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            mTmpX = event.getX();
            mTmpY = event.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            /** * calculate the distance moved * this is a judgment, in case mMoveX or mMoveY is 0, because they will determine the direction of the move. * /
            if(event.getX() ! = mTmpX) { mMoveX = event.getX() - mTmpX; }if(event.getY() ! = mTmpY) { mMoveY = event.getY() - mTmpY; }// Create the animation component
            createAnimationView();
            /** * Calculate the percentage of current position * 0 represents the initial position * 0. X represents the percentage of next page turned * 1 represents the next page turned. * -0. X represents the percentage of pages turned over * -1 represents pages turned over. * /
            float percent = mAnimationView.getAnimationPercent();
            if (isVertical) {
                percent += mMoveY / getHeight();
            } else {
                percent += mMoveX / getWidth();
            }
            // Keep the position between 1 and -1
            if(percent < -1){
                percent = -1;
            }
            else if(percent > 1){
                percent = 1;
            }
            if(canPage(mMoveX, mMoveY, percent)) {
                // Display the animation component if it is not displayed
                if(! isAnimationViewVisible()) { setAnimationViewVisible(true);
                }
                // Load or switch animated pictures
                switchAniamtionBitmap(percent);
                mAnimationView.setAnimationPercent(percent, event, isVertical);
            }
            mTmpX = event.getX();
            mTmpY = event.getY();
            break;
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_OUTSIDE:
            /** * calculate the distance moved * this is a judgment, in case mMoveX or mMoveY is 0, because they will determine the direction of the move. * /
            if(event.getX() ! = mTmpX) { mMoveX = event.getX() - mTmpX; }if(event.getY() ! = mTmpY) { mMoveY = event.getY() - mTmpY; }/** * calculate the end position percentage * 0 represents the starting position * 1 represents the next page. * -1 means turn to the previous page. * /
            float toPercent = 0;
            if (isVertical) {
                toPercent = mMoveY > 0 ? 1 : 0;
            } else {
                toPercent = mMoveX > 0 ? 1 : 0;
            }
            if(mAnimationView.getAnimationPercent() < 0) {// If the page is turned up, the start and end should be 0 and -1
                toPercent -= 1;
            }
            // If the page can be turned, the page turning animation is played
            if(canPage(mMoveX, mMoveY, toPercent)) {
                mAnimationView.startAnimation(isVertical, event, toPercent);
            }
            mMoveX = 0;
            mMoveY = 0;
            break;
    }
    return true;
}
Copy the code

This section is the core of the AnimationListView.

First, analyze the ACTION_MOVE state. The createAnimationView function is called as follows:

private void createAnimationView(a){
    if(mAnimationView == null) {try {
            Constructor<? extends AnimationViewInterface> constructor = animationClass.getConstructor(Context.class);
            mAnimationView = constructor.newInstance(getContext());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    mAnimationView.setOnAnimationViewListener(new OnAnimationViewListener() {
        @Override
        public void pageNext(a) {
            AnimationListView.this.pageNext();
        }
 
        @Override
        public void pagePrevious(a) {
            AnimationListView.this.pagePrevious(); }}); }Copy the code

MAnimationView is an implementation of the AnimationViewInterface interface that handles and displays the dynamic effects of switching. The folding that we’re implementing this time is just one of the effects, but for this interface and implementation, we’ll talk about it later, but for the moment you know that this is a View that shows the dynamic effects.

Since there are multiple subclass implementations of AnimationViewInterface, we use a factory mode that uses reflection to initialize according to the animationClass.

Go back to the ACTION_MOVE code, and judge whether to turn the page up or down according to the sliding direction after the creation is successful, and calculate a percentage according to the moving distance. It then uses a canPage function to determine whether the page can be turned. This function is relatively simple and mainly determines whether the page has reached the beginning or the end. How the canpages is true, you can see, in turn, calls the three functions: setAnimationViewVisible, switchAnimationBitmap and mAnimationView setAnimationPercent.

First look at setAnimationViewVisible:

protected void setAnimationViewVisible(boolean visible) {
    if(mAnimationView == null) {return;
    }
    if (visible) {
        addView((View) mAnimationView, mLayoutParams);
    } else{ removeView((View) mAnimationView); }}Copy the code

This function is also mentioned above, and you can see from the code that it adds or removes a mAnimationView based on visible to show the hidden effect.

Calling this function adds the mAnimationView to the screen at the top level, overwriting the current page.

Then the switchAnimationBitmap function:

private void switchAniamtionBitmap(float percent){
    // If the current state is not flipped, or the flipped direction is changed, the background image needs to be switched
    if(mAnimationView.getAnimationPercent() == 0
            || mAnimationView.getAnimationPercent() * percent < 0) {
        // The foreground is the current page, the second in the cached page
        Bitmap frontBitmap = getViewBitmap(mCacheItems.get(1));
        Bitmap backBitmap = null;
        /** * The background image changes according to the flip direction. * If you want to turn to the previous page, the background is the first one in the cached page * if you want to turn to the next page, the background is the second one in the cached page */
        if (isVertical) {
            backBitmap = getViewBitmap(mCacheItems.get(mMoveY > 0 ? 0 : 2));
        } else {
            backBitmap = getViewBitmap(mCacheItems.get(mMoveX > 0 ? 0 : 2));
        }
        // Initialize the animation componentinitAniamtionView(frontBitmap, backBitmap); }}Copy the code

The getViewBitmap function takes screenshots of the current page and the page to be turned according to the different page turning directions. This is why we added all three cached items to the layout earlier, because we need to add them to the screen to capture the content. As for why we need to take a screenshot, because the layout of each Item can be complicated, and in the effect of folding, we need to divide a page into two parts to deal with the effect separately, which is almost impossible to operate directly on the Item. That’s why mAnimationView must overwrite other views at the top. In fact, when we turn the page we see the mAnimationView, and the real page is hidden underneath.

As for how to achieve a screenshot in getViewBitmap, the code is very simple, we look at the source code.

Take screenshots of the two pages and put them in the mAnimationView, and what to do with those two bitmaps is in the mAnimationView, and with those two bitmaps we can do a lot of things, This is why we spend so much time on AnimationListView, because we will use this class for many different effects.

Finally the mAnimationView setAnimationPercent, set by previous calculated as a percentage of the effect of the moment. Different subclasses of this function implement it differently, more on that later.

The whole ACTION_MOVE process, according to the movement to change the display in real time. When the slide is complete, since the page-turning effect may only be shown at a certain point in the middle, you need to start an animation for the rest of the effect to complete the page-turning, which is what the code in the ACTION_UP state does.

This completes the AnimationListView class, which implements a viewPager-like View and focuses on handling user touch events.

AnimationViewInterface interface

FolioView: AnimationViewInterface FolioView: AnimationViewInterface: AnimationViewInterface: AnimationViewInterface: AnimationViewInterface

public interface AnimationViewInterface {
    /** * Initialize the image *@paramFrontBitmap Foreground images *@paramBackBitmap background image */
    void setBitmap(Bitmap frontBitmap, Bitmap backBitmap);
    boolean isAnimationRunning(a);
 
    /** * Start animation * from current state to toPercent state *@param isVertical
     * @param event
     * @paramFinal position percentage of toPercent animation */
    void startAnimation(boolean isVertical, MotionEvent event, float toPercent);
    float getAnimationPercent(a);
 
    /** * Sets the state of the animation to a frame * used to change the state of the AnimationView in real time while sliding *@paramPercent Specifies the percentage of the current position in the animation@param event
     * @param isVertical
     */
    void setAnimationPercent(float percent, MotionEvent event, boolean isVertical);
    void setDuration(long duration);
    void setOnAnimationViewListener(OnAnimationViewListener onAnimationViewListener);
}
Copy the code

As for the function of these methods, I will not go into details because I can basically guess it from the previous explanation. By implementing this interface, we can not only achieve a fold effect, but actually because of setBitmap we get two bitmaps, and we can use these two bitmaps to achieve any effect we want. In the next article, I will implement a louver effect using the AnimationListView and AnimationViewInterface.

Fold animation analysis

How to achieve the folding effect? In fact, the whole effect of folding is divided into three areas, as shown here

Region 1 is drawn at the top of the front page, and region 2 is drawn at the bottom of the back page, and these two regions are not changed at all.

If it is in the lower part, the lower part of the front page will be drawn; if it is in the upper part, the upper part of the back page will be drawn. In addition, trapezoidal deformation is made to achieve the effect of near large and far small. The differences are shown as follows:

This produces the effect of a fold, and area 3 needs to be moved and changed to the trapezoid size for the moving effect and animation. In fact, there is another region, the shadow region, whose position changes according to the position of region 3, and the opacity of the shadow changes with it.

Fold effect drawing

The drawing code is as follows:

@Override
protected void onDraw(Canvas canvas) {
    if (mFrontBitmapTop == null || mBackBitmapTop == null) {
        return;
    }
    if(getHeight() <= 0) {return;
    }
    /** * Calculate the ratio of rollover * used to calculate the stretch and shadow effect of the image */
    float rate;
    if (mFolioY >= getHeight() / 2) {
        rate = (float) (getHeight() - mFolioY) * 2 / getHeight();
    } else {
        rate = (float) mFolioY * 2 / getHeight();
    }

    / * * * * according to turn the judgment on the up and down pictures /
    Bitmap topBitmap = null;
    Bitmap bottomBitmap = null;

    Bitmap topBitmapFolie = null;
    Bitmap bottomBitmapFolie = null;
    if(mCurrentPercent < 0){
        topBitmap = mFrontBitmapTop;
        bottomBitmap = mBackBitmapBottom;
        topBitmapFolie = mFrontBitmapBottom;
        bottomBitmapFolie = mBackBitmapTop;
    }
    else if(mCurrentPercent > 0){
        topBitmap = mBackBitmapTop;
        bottomBitmap = mFrontBitmapBottom;
        topBitmapFolie = mBackBitmapBottom;
        bottomBitmapFolie = mFrontBitmapTop ;
    }
    if (topBitmap == null || bottomBitmap == null) {
        return;
    }
    /** * Draw the topBitmap in the top half */
    Rect topHoldSrc = new Rect(0.0, topBitmap.getWidth(), topBitmap.getHeight());
    Rect topHoldDst = new Rect(0.0, getWidth(), getHeight() / 2);
    canvas.drawBitmap(topBitmap, topHoldSrc, topHoldDst, null);

    /** * draw the bottomBitmap */
    Rect bottomHoldSrc = new Rect(0.0, bottomBitmap.getWidth(), bottomBitmap.getHeight());
    Rect bottomHoldDst = new Rect(0, getHeight() / 2, getWidth(), getHeight());
    canvas.drawBitmap(bottomBitmap, bottomHoldSrc, bottomHoldDst, null);

    /** * draw shadows * Shadows are in the same area as the flip and change according to the flip degree */
    Paint shadowP = new Paint();
    shadowP.setColor(0xff000000);
    shadowP.setAlpha((int) ((1 - rate) * FOLIO_SHADOW_ALPHA));
    if (mFolioY >= getHeight() / 2) {
        canvas.drawRect(bottomHoldDst, shadowP);
    } else {
        canvas.drawRect(topHoldDst, shadowP);
    }

    /** * the inverted image is a trapezoid, depending on the situation of the trapezoid size and position are different */
    mFolioBitmap = null;
    float[] folioSrc = null;
    float[] folioDst = null;
    if (mFolioY >= getHeight() / 2) {
        // when the flip position is lower than the middle, take topBitmapFolie and draw the region as a normal trapezoid
        mFolioBitmap = topBitmapFolie;
        folioDst = new float[] {0, getHeight() / 2,
                getWidth(), getHeight() / 2,
                rate * FOLIO_SCALE * getWidth() + getWidth(), mFolioY,
                -rate * FOLIO_SCALE * getWidth(), mFolioY};
    } else {
        // when the flip position is above the middle, take bottomBitmapFolie and draw the region as an inverted trapezoid
        mFolioBitmap = bottomBitmapFolie;
        folioDst = new float[]{
                -rate * FOLIO_SCALE * getWidth(), mFolioY,
                rate * FOLIO_SCALE * getWidth() + getWidth(), mFolioY,
                getWidth(), getHeight() / 2.0, getHeight() / 2
        };
    }
    folioSrc = new float[] {0.0,
            mFolioBitmap.getWidth(), 0,
            mFolioBitmap.getWidth(), mFolioBitmap.getHeight(),
            0, mFolioBitmap.getHeight()};
    Matrix matrix = new Matrix();
    matrix.setPolyToPoly(folioSrc, 0, folioDst, 0, folioSrc.length >> 1);
    canvas.drawBitmap(mFolioBitmap, matrix, null);

    super.onDraw(canvas);
}
Copy the code

You can see that the mFolioY parameter is the key. This parameter is the distance from the trapezoidal edge of area 3 to the top of the page. This parameter is used to calculate the position of region 3, the size of the shadow, the shape of the trapezoid, and so on.

In the drawing process, regions 1 and 2 are drawn first, because these two regions are fixed and unaffected by other parameters.

Then determine whether region 3 is in the upper or lower part according to mFolioY. First draw a shadow, the shadow area is in the same part as area 3, using a simple method, completely cover area 1 or 2.

Then draw region 3 to cover the shaded areas. Different pictures were selected by judging the position of region 3, and the Matrix and Matrix were used to make trapezoidal deformation of the pictures, and then the pictures were drawn to the specified region.

This is the whole drawing process, when we change the parameter mFolioY and redraw the page to create the effect of movement.

Fold animation parsing in half

From the previous analysis, we know that the whole movement process actually has two stages: manual and automatic. The manual phase moves with the touch move event and automatically animates when the touch ends.

The manual stage

This is done by calling the setAnimationPrecent function of the AnimationViewInterface:

public void setAnimationPercent(float percent, MotionEvent event, boolean isVertical) {
    if(! isVertical){return;
    }
    if(getHeight() <= 0) {return;
    }
    /** * Calculates the position of the flip * if the position is outside the region, the flip is completed */
    mFolioY = percent > 0 ? percent * getHeight() : (1 + percent) * getHeight();
    invalidate();
    mCurrentPercent = percent;
}
Copy the code

And you can see that basically you compute mFolioY from percent, and then you redraw it.

Automatic phase

Call another function, startAnmation, as follows

public void startAnimation(boolean isVertical, MotionEvent event, final float toPercent) {
    if(! isVertical){return;
    }
    if(getHeight() <= 0) {return;
    }
    /** * Play the flip animation * calculate the end position of the animation, and then set the animation to flip from the current position to the end point * the animation is essentially changing the flip position and redrawing */
    float endPosition = 0;
    if (mCurrentPercent < 0) {
        endPosition = toPercent == 0 ? getHeight() : 0;
    } else{
        endPosition = toPercent == 0 ? 0 : getHeight();
    }
    mFolioAnimation = ObjectAnimator.ofFloat(this."folioY", endPosition);
    mFolioAnimation.setDuration((long)(mduration * Math.abs(toPercent - mCurrentPercent)));
    mFolioAnimation.addListener(new Animator.AnimatorListener() {
        @Override
        public void onAnimationStart(Animator animation) {}@Override
        public void onAnimationEnd(Animator animation) {
            mCurrentPercent = 0;
            if(mOnAnimationViewListener ! =null) {if(toPercent == 1){
                    mOnAnimationViewListener.pagePrevious();
                }
                else if(toPercent == -1){ mOnAnimationViewListener.pageNext(); }}}@Override
        public void onAnimationCancel(Animator animation) {}@Override
        public void onAnimationRepeat(Animator animation) {}}); mFolioAnimation.start(); }Copy the code

ToPercent computes endPosition, which is the value of mFolioY at the end of the animation.

Then launch a property animation that increments the value of mFolioY from its current value to endPosition through the setter and getter. When the animation ends, determine the page turning direction and call the corresponding method of listener to realize the page switching.

conclusion

In summary, the effect of folding in half is not difficult to use, whether drawing or property animation, relatively easy to use. This article mainly introduces such a framework, on the basis of which we will later implement some more complex effects, such as the shutter effect in the next article.

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