Background: fast New Year, the problem that much ah, recently the hand all knock calluses, go to work dozen a card all want to identify a few minutes, do not know as a program ape of you whether have the same feeling. I still have over 200 bugs to my name…

Layout of the article
Home Page Interactive

1. Enter and exit

Github.com/zibuyuqing/…

The implementation of this function is more complex, compared to explore such as the TAB style, need to pay attention to many places, this article I will take you step by step to achieve these effects, of course, the solution is a personal idea, not only this kind of ha. The narrative line is as follows:

(1) Component structure design

(2) Define the stack view

(3) Vertical gesture processing

(4) Horizontal gesture processing

(5) Add and delete page logic

(6) Transition animation implementation

Due to limited space, this article will cover vertical gesture processing first (the article is very detailed, I tried it, it will be long if finished, I have been working on it all afternoon).

Component structure design

The structure design

Mode: Adapter mode, observer mode; Reference model: RecyclerView

The essence of stacked View is a View container, is a List, based on this, we want to design this component, naturally come to think of the adapter mode, Android View container is the most used is Listview, RecyclerView and inherit from AdapterView GridView, etc. Of course, my favorite is RecyclerView, so let’s refer to this force to do a simple. Main components: StackView (StackView container, class RecyclerView), ViewHolder, Adapter.

The specific implementation

Adapter

See RecyclerView Adapter, write an abstract class, and think about what methods we need:

(1) Create view

(2) Obtain the number of data or subitems

(3) Get the binding view type

(4) Monitor data changes

With these methods implemented, a basic Adapter is implemented. Let’s look at the code:

Public static abstract class Adapter<VH extends ViewHolder> {private final AdapterDataObservable Observable = new AdapterDataObservable(); Public VH createView(ViewGroup parent, int viewType) {VH holder = onCreateView(parent, viewType); holder.itemViewType = viewType;returnholder; } protected abstract VH onCreateView(ViewGroup parent, int viewType); // Bind view public voidbindViewHolder(VH holder, int position) { onBindViewHolder(holder, position); } protected abstract void onBindViewHolder(VH holder, int position); Public abstract int getItemCount(); public final voidnotifyDataSetChanged() {
            observable.notifyDataChanged();
        }
        
        public int getItemViewType(int position) {
            return0; } / / registered observers public void registerObserver (AdapterDataObserver observer) {observables. RegisterObserver (observer); }}Copy the code

There’s a defined data target AdapterDataObservable. Look at its implementation

Public static class AdapterDataObservable extends Observable<AdapterDataObserver> {// mObservers Public BooleanhasObservers() {
            return! mObservers.isEmpty(); } // Notify observers of public voidnotifyDataChanged() {
            for(AdapterDataObserver observer : mObservers) { observer.onChanged(); }}}Copy the code

Gnome male – “… Observer mode, where there is a data object (observed), there is also the observer

    public static abstract class AdapterDataObserver {
        public void onChanged() {

        }
    }

    private class ViewDataObserver extends AdapterDataObserver {
        @Override
        public void onChanged() { refreshViews(); }}Copy the code

Here do more violence, there is data update, immediately update all view, as a single update view method, I leave you to implement it.

ViewHolder

Any monkey that used RecyclerView could probably do it

    public static abstract class ViewHolder {
        public View itemView;
        public int itemViewType;
        int position;

        public ViewHolder(View view) {
            itemView = view;
        }

        public Context getContext() {
            returnitemView.getContext(); }}Copy the code
Bean

Here I want to think about our data model according to actual business, open the orthodox phone UC browser, enter the page management interface, titanium dog eye I found a page of title, page preview, website icon, in order to distinguish between different pages, we also need to give each page a Key, is good, the data model is set up

public class UCPager { private String title; private int websiteIcon; // The site icon is more reasonable to download in the cloud, for convenience, I first use the local private Bitmap pagerPreview; private int key; public UCPager(String title, int websiteIcon, Bitmap pagerPreview,int key) { this.title = title; this.websiteIcon = websiteIcon; this.pagerPreview = pagerPreview; this.key = key; }... }Copy the code
Item tamplate

At first I thought that each page was a UCRootView (root layout), which would be too much memory, so I opened the page management interface again and looked at the layout of a page using DDMS

<? xml version="1.0" encoding="utf-8"? > <com.zibuyuqing.ucbrowser.widget.stackview.UCPagerView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ImageView
        android:id="@+id/ivPagePreview"
        android:scaleType="centerCrop"
        android:layout_gravity="center"
        android:src="@drawable/test_uc_screen"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
    <RelativeLayout
        android:id="@+id/rlPageHead"
        android:background="@color/windowBg"
        android:layout_width="match_parent"
        android:layout_height="@dimen/dimen_48dp">
        <ImageView
            android:id="@+id/ivWebsiteIcon"
            android:padding="12dp"
            android:scaleType="centerCrop"
            android:src="@drawable/ic_home"
            android:layout_width="@dimen/dimen_48dp"
            android:layout_height="match_parent" />
        <TextView
            android:id="@+id/tvPagerUC"
            android:textSize="20dp"
            android:layout_centerVertical="true"
            android:layout_toRightOf="@id/ivWebsiteIcon"
            android:text="UC"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
        <ImageView
            android:id="@+id/ivPageClose"
            android:layout_alignParentRight="true"
            android:padding="14dp"
            android:src="@drawable/ic_close"
            android:layout_width="@dimen/dimen_48dp"
            android:layout_height="match_parent" />
    </RelativeLayout>
</com.zibuyuqing.ucbrowser.widget.stackview.UCPagerView>
Copy the code

Defining a stack view

Design idea

In my opinion, all interface changes can be controlled by proportion. In the first article of this series, “Trying to Write a UC Browser (Layout)”, we introduced sliding processing in UCRootView based on proportion (rate), and then we used this rate thoroughly in Interaction. Interested partners can have a look. If you can, do it. In StackView I give this ratio a nice name: Progress! The user initializes progress when entering the interface, updates progress when sliding and uses it to check whether it is overscroll. After lifting the finger, the user compares the current progress with the target progress, and then automatically slides to the corresponding position.

Differences in implementation

So we have to think, why can different pages appear in different places? TranslationY! Why do different pages have different sizes? Scale! Why this cool effect? TranslationY + Scale!

Calculate TranslationY
/** * Calculate the view TransY, first according to the reference progress, to calculate the offset progress of each view, and then offset the progress to the fourth power to expand the difference * finally in the target TransY * mViewMin* mViewMaxTop * @param I view index * @param progress reference progress */ int calculateProgress2TransY(int i,float progress) {
        return (int) (mViewMinTop +
                Math.pow(calculateViewProgress(i,progress),4) * (mViewMaxTop - mViewMinTop));
    }

    int calculateProgress2TransZ(float progress) {
        return (int) (mViewMinTop + Math.pow(progress, 3) * (200));
    }

Copy the code

Here we use the fourth power, which is the best result THAT I have determined after countless trials (only four). If you don’t use the fourth power, it looks like this:

/** * Used to calculate the sliding progress of each view * @param index view position * @param progress reference progress * @return
     */
    private float calculateViewProgress(int index,float progress) {
        return PROGRESS_STEP * index + progress;
    }
Copy the code

Increments PROGRESS_STEP (0.2f, of course you can change it to another value) according to the view index, and then increments the PROGRESS_STEP to our reference scale.

Computing Scale
/** * calculate scale * mViewMaxScale to view maximum scale * mViewMinScale is the smallest Scale of the view * @param I view position * @param progress reference progress */float calculateProgress2Scale(int i,float progress) {
        float scaleRange = (mViewMaxScale - mViewMinScale);
        return mViewMinScale + (calculateViewProgress(i,progress) * scaleRange);
    }
Copy the code

The calculateViewProgress () method is also used here, but not to the fourth power.

Set the child View properties

With the values, we need to associate them with the view, and setting the view properties is one of the core ways to stack views, which is the foundation of all effects. Let’s take a look

    private void layoutChildren() {
        int childCount = getChildCount();
        float progress;
        float transY;
        floattransZ; View child; mChildTouchRect = new Rect[childCount]; // The touch range of the child view is log.e (TAG,"layoutChildren :: layoutChildren :: mLayoutState =:" + mLayoutState);
        for(int i = 0; i < childCount; i++) { child = getChildAt(i); Rect Rect = new Rect(); child.getHitRect(rect); mChildTouchRect[i] = rect; // Use switch (mLayoutState){switch (mLayoutState){case LAYOUT_PRE_ACTIVE:
                    if(i > mActivePager){
                        continue;
                    }
                    break;
                case LAYOUT_AFTER_ACTIVE:
                    if(i < mActivePager){
                        continue;
                    }
            }
            progress = getScrollP();
            transY = calculateProgress2TransY(i,progress);
            transZ = calculateProgress2TransZ(progress);
            Log.e(TAG, "layoutChildren :: progress =:" + progress + ",transY =:" + transY);
            translateViewY(transY, child);
            //translateViewZ(transZ, child);
            scaleView(calculateProgress2Scale(i,progress), child);
        }
        invalidate();
    }
Copy the code

MChildTouchRect is an array of RecTs that record the drawing range of each view. This is a reference that we use to identify sub-views when we’re doing gestures. It’s important that we update this array in real time as the view properties change. By setting the TranslationY and Scale of the view respectively, we can achieve the following effect:

Vertical gesture processing

We’ve been talking about progress, but where did this progress come from? The answer, as we explained in Layout, is determined by the ratio of the sliding distance to the target distance.

Sliding detection

In the onInterceptTouchEvent method, check whether to intercept — return true if the sliding animation is executing or if the finger moves beyond our specified threshold; If true, we will process the event in this layer, at which point onTouchEvent is executed. OnInterceptTouchEvent onTouchEvent onInterceptTouchEvent onTouchEvent onTouchEvent onTouchEvent onTouchEvent

            caseACTION_DOWN: {// Record the initial touch point mInitialMotionX = mLastMotionX = (int) ev.getx (); mInitialMotionY = mLastMotionY = (int) ev.getY(); mActivePointerId = ev.getPointerId(0); // If already rolling, stop him stopScroller(); / / initialize the speed tracker initOrResetVelocityTracker (); mVelocityTracker.addMovement(ev); // Disallow parents from intercepting move eventsbreak; } // Handle multiple fingerscase MotionEvent.ACTION_POINTER_DOWN: {
                final int index = ev.getActionIndex();
                mActivePointerId = ev.getPointerId(index);
                mLastMotionX = (int) ev.getX(index);
                mLastMotionY = (int) ev.getY(index);
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                if (mActivePointerId == INVALID_POINTER) break;
                Log.e(TAG, "onTouchEvent :: ACTION_MOVE = ");
                mVelocityTracker.addMovement(ev);

                int activePointerIndex = ev.findPointerIndex(mActivePointerId);
                int x = (int) ev.getX(activePointerIndex);
                int y = (int) ev.getY(activePointerIndex);
                int yTotal = Math.abs(y - (int) mInitialMotionY);
                float deltaP = mLastMotionY - y;
                if(! mIsScrolling) {if (yTotal > mTouchSlop) {
                        mIsScrolling = true; }}if(mIsScrolling) {// mTotalMotionY is the total scrolling distanceif(isOverPositiveScrollP()) {// calculateDamping() is the method to calculateDamping, that is, when overscroll, MTotalMotionY -= deltaP *(calculateDamping()); }else{ mTotalMotionY -= deltaP; } // Update the viewdoScroll();
                }

                mLastMotionX = x;
                mLastMotionY = y;
                break;
            }
Copy the code

What should we do when the user leaves the screen with their finger (ACTION_UP, ACTION_POINTER_UP) or cances an action (ACTION_CANCEL)?

(1) Update the touch point information if one of the multiple fingers leaves the screen

(2) If the finger is moving very fast, let it fly for a while

(3) Slide from the current position to the reasonable position specified by us

(4) Reset the sliding state

          caseMotionEvent.ACTION_UP: { mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); int velocity = (int) mVelocityTracker.getYVelocity(mActivePointerId); // When the speed is high, execute the scroll.fling () method to let the interface run for a whileif (mIsScrolling && (Math.abs(velocity) > mMinimumVelocity)) {
                    fling(velocity);
                } else{// Slide to target position scrollToPositivePosition(); } // resetTouchState(); Log.e(TAG,"onTouchEvent :: mIsOverScroll =:" + mIsOverScroll);
                break; } // Update the touch informationcase MotionEvent.ACTION_POINTER_UP: {
                int pointerIndex = ev.getActionIndex();
                int pointerId = ev.getPointerId(pointerIndex);
                if(pointerId == mActivePointerId) { // Select a new active pointer id and reset the motion state final int newPointerIndex  = (pointerIndex == 0) ? 1:0; mActivePointerId = ev.getPointerId(newPointerIndex); mLastMotionX = (int) ev.getX(newPointerIndex); mLastMotionY = (int) ev.getY(newPointerIndex); mVelocityTracker.clear(); }break;
            }
            case MotionEvent.ACTION_CANCEL: {
                scrollToPositivePosition();
                resetTouchState();
                break;
            }
Copy the code

Scroll to a position we will explore in the Overscroll test. Let the page fly for a while. Obviously, use scroll.fling () :

/** * If we swipe our finger away from the screen quickly and let the view fly for a while, Public void fling(0, (int) mScroller. Fling (0, (int) mTotalMotionY, 0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE); invalidate(); }Copy the code

Here the scroller is slipping, but we need to update the view!! How to do? Rewrite computeScroll ()

    @Override
    public void computeScroll() {
        Log.e(TAG, "computeScroll :: mIsOverScroll :" + mIsOverScroll);
        if (mScroller.computeScrollOffset()) {
            if(mIsOverScroll){// If the overscroll slides to the specified position scrollToPositivePosition(); }else {
                if(mScroller.isFinished()){
                    scrollToPositivePosition();
                }
                mTotalMotionY = mScroller.getCurrY();
                doScroll();
            }
        }
        super.computeScroll();
    }

Copy the code

The doScroll method does two things — check for overscroll and update the view property (we introduced layoutChildren () earlier).

/** */ private voiddoScroll() { computeScrollProgress(); OverScroll layoutChildren(); // Change the properties of each view} /** ** @returnWhether the specified position is exceeded */ private BooleancomputeScrollProgress() {
        if (getChildCount() <= 0) {
            return false;
        }
        mIsOverScroll = false; mScrollProgress = getScrollRate(); / / update the progress mIsOverScroll = (mScrollProgress > mMaxScrollP | | mScrollProgress < mMinScrollP);
        return mIsOverScroll;
    }
Copy the code

Finally see that powerful progress, dangdang….

/ * * * @returnRatio of distance moved to target distance */ privatefloat getScrollRate() {
        floattopSpace = mViewMaxTop; // mViewMaxTop is the screen heightreturn mTotalMotionY / topSpace;
    }
Copy the code

Ok, we continuously: the whole process is: judge whether to intercept events — “consumption events –” real-time calculation of sliding distance — “check whether OverScroll –” update progress — “update view. , of course, here I only sliding condition are introduced, and illustrate some of the code, if you look a little meng or want to dig into, please: https://github.com/zibuyuqing/UCBrowser

Overscroll detection

If the UC browser slides beyond a certain range, it will be more and more difficult. In addition, when the UC browser slides to the bottom or top, it will slide back to the specified position, which is useful for overscroll detection.

Check whether over is detected.

First of all, when the number of child views changes, we do not adjust the reference progress. At this point we need to update the sliding range

/** * We will update the sliding range */ private voidupdateScrollProgressRange(){
        mMinScrollP = BASE_MIN_SCROLL_P - (getChildCount() - 2) * PROGRESS_STEP;
        mMaxScrollP = BASE_MAX_SCROLL_P;
        mMinPositiveScrollP = mMinScrollP + PROGRESS_STEP * 0.25f; MMaxPositiveScrollP = mmaxScrollp-progress_step * 0.75f; Log.e(TAG,"updateScrollProgressRange ::mMinScrollP =:" + mMinScrollP +",mMaxScrollP =:" + mMaxScrollP);
    }
Copy the code

The constant inside is determined by me after thousands of tests, ha ha, is also tired. Given the sliding range, how do we detect if we’re out of range?

mIsOverScroll = (mScrollProgress > mMaxScrollP || mScrollProgress < mMinScrollP);
Copy the code

Ha ha, vomited blood, originally so simple. A careful monkey can see that the above code has two variables, mMinPositiveScrollP and mMaxPositiveScrollP. These two values are the threshold for preventing the user from sliding. When the user slides beyond these two values, the sliding becomes more and more difficult. When the user’s finger leaves the screen, the child view automatically scrolls to the appropriate position (the first code is in OnTouchEvent).

                if(mIsScrolling) {// mTotalMotionY is the total scrolling distanceif(isOverPositiveScrollP()) {// calculateDamping() is the method to calculateDamping, that is, when overscroll, MTotalMotionY -= deltaP *(calculateDamping()); }else{ mTotalMotionY -= deltaP; } // Update the viewdoScroll();
                }
Copy the code
   boolean isOverPositiveScrollP() {return (mScrollProgress > mMaxPositiveScrollP || mScrollProgress < mMinPositiveScrollP);
    }
Copy the code
/** * Calculate the damping and make the user feel "labored" when sliding over the position we setreturn
     */
    private float calculateDamping() {floatDamping = (1.0 f-math.abs (mscrollProgress-getPositivesCrollp ()) * 5); Log.e(TAG,"calculateDamping :: damping = :" + damping);
        return damping;
    }
Copy the code
Automatically scroll to the specified position

When the user’s finger leaves the screen, if the overscroll, we will automatically scroll to the specified position

(1) Get target progress

/** * Based on the progress of the slide to determine the target progress to be automatically rolled back after finger release */float getPositiveScrollP() {
        if (mScrollProgress < mMinPositiveScrollP) {
            return mMinPositiveScrollP;
        } else if(mScrollProgress > mMaxPositiveScrollP){
            return mMaxPositiveScrollP;
        }
        return mScrollProgress;
    }
Copy the code

(2) Define scroll back drawing

/** * After the finger is released, if the finger is not in the desired position (such as over), * @param curScroll current progress * @param newScroll target progress * @param postRunnable action to be performed after rolling to target position */ void animateScroll(float curScroll, floatnewScroll, final Runnable postRunnable) { // Finish any current scrolling animations stopScroller(); MScrollAnimator = objectAnimator.offloat (this,"scrollP", curScroll, newScroll); / / animation time mScrollAnimator setDuration (mDuration); . / / interpolator mScrollAnimator setInterpolator (mLinearOutSlowInInterpolator); mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener{@override public void onAnimationUpdate(ValueAnimator ValueAnimator) {// Update progresssetScrollP((Float) valueAnimator.getAnimatedValue()); }}); mScrollAnimator.addListener(newAnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                if (postRunnable != null) {
                    postRunnable.run();
                }
                mScrollAnimator.removeAllListeners();
            }
        });
        mScrollAnimator.start();
    }
Copy the code
    public void setScrollP(float progress) {
        Log.e(TAG, "rate =:"+ progress); mTotalMotionY = calculateProgress2Y(progress); MScrollProgress = progress; layoutChildren(); }Copy the code

There is a transition from Progress to mTotalMotionY. I was trying to make sure that progress didn’t jump after the view was added or deleted

/** * Restore the sliding distance according to our reference progress * @param progress * @return
     */
    private float calculateProgress2Y(float progress) {
        return progress * mViewMaxTop;
    }
Copy the code

Isn’t it easy? Ha ha. Blood on your face.

(3) Perform rollback

/** * Roll finger off screen to target */ private voidscrollToPositivePosition() {
        Log.e(TAG, "scrollToPositivePosition mScrollProgress =:" + mScrollProgress);
        float curScroll = getScrollP();
        floatpositiveScrollP = getPositiveScrollP(); // Execute if current progress and target progress are differentif(Float.compare(curScroll,positiveScrollP) ! = 0) { animateScroll(curScroll, getPositiveScrollP(), newRunnable() {
                @Override
                public void run() {// resetTouchState(); }}); invalidate(); }}Copy the code

github

Please specify: juejin.cn/post/684490… .

Attachment: In the next chapter, we will realize the functions of horizontal sliding to delete pages, including click and click delete, blank page detection, and transition animation (to be completed within two days).

In the next installment, we will explore the implementation of drag-and-drop views, which looks like this

Series of articles:

Try writing a UC browser (Layout)

Try writing a UC browser (home page interactive)

Project address: github.com/zibuyuqing/…