Sliding back to SwipeBackActivity

Keywords: Android xml encoding REST

Sliding back to SwipeBackActivity

Careful students can find that the sliding return of Wechat is actually a little more detailed than the normal sliding return. The returned Activity will show a part of it, and then follow the linkage, which used to be a direct two-connected sliding. This article talks about how to simulate the sliding return function of a Wechat.

Slip into

If only sliding back, the user can hardly find this function, but if the new Activity is shown by sliding from the right side to the left side of the screen, then the feeling that sliding out of the Activity on the left side of the screen is natural. This is some knowledge of interaction, so we need to make the effect of sliding into the new Activity first.

This effect is very good to do, the basic method on the Internet is to use the override Pending Transition () method, and then use the project that has been written, xml file to load a transition, which can be achieved.

    @Override  
    public void startActivity(Intent intent) {  
        super.startActivity(intent);  
        overridePendingTransition(R.anim.in_right, R.anim.out_left);  
    }  
R.anim.in_right.xml

    <?xml version="1.0" encoding="utf-8"?>  
    <set xmlns:android="http://schemas.android.com/apk/res/android">  
        <translate android:fromXDelta="100%p" android:toXDelta="0"  
            android:duration="@android:integer/config_shortAnimTime" />  
    </set>  
R.anim.out_left.xml

    <?xml version="1.0" encoding="utf-8"?>  
    <set xmlns:android="http://schemas.android.com/apk/res/android">  
        <translate android:fromXDelta="0" android:toXDelta="-100%p"  
            android:duration="@android:integer/config_shortAnimTime" />  
    </set>  

The principle is also very simple. The original Activity moves from 0 to - 100%p, which is the width of a screen. Moving in the negative direction is moving to the left, so it slides out from the left. The new Activity slides from 100%p to 0 on the right, and then slides from the right. The two animations will be synchronized so that the effect of sliding in can be achieved.

Slip back

The basic principle is that the registerActivityLifecycle Callbacks (ActivityLifecycle Callbacks) method of the Application class can record the life cycle of all activities globally, so we can use this to store all our activities and a stack, take out the previous activity from the stack each time the sliding returns, and then separate the id from the window.id_ANDROID_CONT. ENT's FrameLayout, which is the parent view of the view in our setContentView, can be used to obtain the View displayed in the Activity interface. Then we listen for gesture events, load the View of the previous activity into the slider and constantly change its offset.

The first is to implement the Activity Lifecycle Callbacks interface and store all our Activities in one stack:

public class ActivityLifeCycleHelper implements Application.ActivityLifecycleCallbacks {
    private Stack<Activity> mActivities;

    public ActivityLifeCycleHelper(){
        mActivities=new Stack<>();
    }

    @Override
    public void onActivityCreated(Activity activity, Bundle bundle) {
        mActivities.add(activity);
    }

    @Override
    public void onActivityStarted(Activity activity) {
    }

    @Override
    public void onActivityResumed(Activity activity) {
    }

    @Override
    public void onActivityPaused(Activity activity) {
    }

    @Override
    public void onActivityStopped(Activity activity) {
    }

    @Override
    public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
    }

    @Override
    public void onActivityDestroyed(Activity activity) {
        mActivities.remove(activity);
    }

    public Activity getPreActivity(){
        int size=mActivities.size();
        if(size<2) return null;
        else return mActivities.get(size-2);
    }

    public void removeActivity(Activity activity){
        mActivities.remove(activity);
    }
}

As you can see, this class is very simple, just add the stack when Activity is created and remove it when it is destroyed. But this class can also expand a lot of other functions, such as making a task manager, one-click clearance of all activities and so on, which is not detailed here.

Then you call the registerActivityLifecycle Callbacks () method in Application:

public class MyApplication extends Application {
    public ActivityLifeCycleHelper getHelper() {
        return mHelper;
    }

    private ActivityLifeCycleHelper mHelper;
    @Override
    public void onCreate() {
        super.onCreate();
        mHelper=new ActivityLifeCycleHelper();
        //store all the activities
        registerActivityLifecycleCallbacks(mHelper);
    }
}

Then we define a basic SweepeBackActivity, which is inherited from AppCompactActivity of course. What we need to do is to rewrite its dispatchTouchEvent() method. This is because we need to monitor the boundary sliding return time and must intercept some of the touch events. Otherwise, if there are some controls such as recyclerview, the touch events will conflict. So we need to intercept some touch events and deal with them first.

public class SwipeBackActivity extends AppCompatActivity {
    private TouchHelepr mTouchHelepr;

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if(mTouchHelepr==null)
            mTouchHelepr=new TouchHelepr(getWindow());
        boolean consume=mTouchHelepr.processTouchEvent(ev);
        if(!consume) return super.dispatchTouchEvent(ev);
        return false;
    }
}

Here is a class TouchHelper written by ourselves, in which specific logical operations are implemented. Next comes the key class TouchHelper.

Touch event handling

First, we define three states:

    private boolean isIdle=true;
    private boolean isSlinding=false;
    private boolean isAnimating=false;
  1. isIdle, indicating that the current state is static
  2. isSliding, which means that the current user's finger moves, and our View slides with it
  3. IsAnimating, which means that the user's finger is released and the View either restores or moves to the right and disappears, is an Animation process, isAnimating = true means that the current Animation process is in progress.

Then there are several member variables.

    private Window mWindow;
    private ViewGroup preContentView;
    private ViewGroup curContentView;
    private ViewGroup curView;
    private ViewGroup preView;
    private Activity preActivity; 

    //Shadow class, I'll talk about it later.
    private ShadowView mShadowView;

    //Left trigger width
    private int triggerWidth=50;
    //Shadow width
    private int SHADOW_WIDTH=30;

MWindows is used to initialize TouchHelper, and this window contains context, activity and other information.

curContentView and preContentView represent the FrameLayout in the outer layer of the current and previous Activities respectively.

curView and preView represent the current and previous Activity's interface views respectively.

Then there's the code for handling gestures:

public TouchHelper(Window window) {
        mWindow = window;
    }

    private Context getContext() {
        return mWindow.getContext();
    }

    public boolean processTouchEvent(MotionEvent event) {
        if (isAnimating) {
            return true;
        }

        float x = event.getRawX();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (x <= triggerWidth) {
                    isIdler = false;
                    isSliding = true;
                    startSlide();
                    return true;
                }
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                if (isSliding) {
                    return true;
                }
                break;
            case MotionEvent.ACTION_MOVE:
                if (isSliding) {
                    if (event.getActionIndex() != 0) {
                        return true;
                    }
                    sliding(x);
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                if (!isSliding) {
                    return false;
                }

                int width = getContext().getResources().getDisplayMetrics().widthPixels;
                isAnimating = true;
                isSliding = false;
                startAnimating(width / x <= 3, x);
                return true;
            default:
                break;
        }
        return false;
    }

In the function processTouchEvent(), we return true where we want to intercept, so that the sub-View will not be touched by the event, and the rest should return false, indicating that the touching event will be distributed to the sub-View for processing.

The code for state change is relatively simple, so it is not explained. This paper mainly talks about several operation functions with the change of state:

  1. startSlide()
  2. sliding(x)
  3. startAnimating(width / 3 <= 3, x)

startSlide()

As the name implies, start sliding, first look at the code:

public void startSlide() {
        preActivity = ((MyApplication) getContext().getApplicationContext()).getHelper().getPreActivity();
        if (preActivity == null) {
            return;
        }

        preContentView = (ViewGroup) preActivity.getWindow().findViewById(Window.ID_ANDROID_CONTENT);
        preView = (ViewGroup) preContentView.getChildAt(0);
        preContentView.removeView(preView);
        curContentView=(ViewGroup) mWindow.findViewById(Window.ID_ANDROID_CONTENT);
        curView= (ViewGroup) curContentView.getChildAt(0);
        preView.setX(-preView.getWidth() / 3);
        curContentView.addView(preView, 0);

        if (mShadowView == null) {
            mShadowView = new ShadowView(getContext());
        }
        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(SHADOW_WIDTH, FrameLayout.LayoutParams.MATCH_PARENT);
        curContentView.addView(mShadowView, 1, params);
        mShadowView.setX(-SHADOW_WIDTH);
    }

In startSlide(), we assign values to several member variables, which is actually an initialized function. First we get the previous Activity. If there is no Activity in front of us, we can't return it, so we return directly. Then we get the parent View from the preActivity, and then we get the first child View, which is the View of the previous Activity we need, and add the preView to curContentVie. In w, and give it an initial offset, the first step of linkage effect can be achieved here. Special attention should be paid to the index encroachment in addView(view, index). The larger the index parameter value, the more backward the representation is drawn. When preView is added here, the index is 0, indicating that the preView is drawn first, otherwise the preView will be displayed on the curView, which is incorrect. And suddenly mShadowView appears, you will find that if Wechat slides, there is a layer of shadow on the edge, which is added here.

sliding()

private void sliding(float rawX) {
        if(preActivity==null) return;
        curView.setX(rawX);
        preView.setX(-preView.getWidth()/3+rawX/3);
        mShadowView.setX(-SHADOW_WIDTH+rawX);
    }

This function is much simpler, because it changes curView, preView and mShadowView dynamically with the position of the user's finger.

startAnimating()

public void startAnimating(final boolean isFinishing, float x) {
        int width = getContext().getResources().getDisplayMetrics().widthPixels;
        ValueAnimator animator = ValueAnimator.ofFloat(x, isFinishing ? width : 0);
        animator.setInterpolator(new DecelerateInterpolator());
        animator.setDuration(200);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                sliding((Float) animation.getAnimatedValue());
            }
        });
        animator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {
                doEndWorks(isFinishing);
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
        animator.start();
    }

When the x coordinate of the position of the user's finger is less than 1/3 of the screen width, the original state is restored. Otherwise, the preView will be fully displayed. Here, we use Value Animator to realize animation. Note that after the animation is completed, we still need to do some finishing work, which is the method doEndWorks().

doEndWorks()

    private void doEndWorks(boolean isFinishing) {
        if (preActivity == null) {
            return;
        }

        if (isFinishing) {
            BackView view = new BackView(getContext());
            view.cacheView(preView);
            curContentView.addView(view, 0);
        } else {
            preView.setX(0);
        }

        curContentView.removeView(mShadowView);
        if (curContentView == null || preContentView == null) {
            return;
        }

        curContentView.removeView(preView);
        preContentView.addView(preView);
        if (isFinishing) {
            ((Activity) getContext()).finish();
            ((Activity) getContext()).overridePendingTransition(0, 0);
        }

        isAnimating=false;
        isSliding=false;
        isIdler=true;
        preView=null;
        curView=null;
    }

At the end of the work, we modify the status, remove the removed View, and add the added View. If preView is fully displayed, you should also use ((Activity) getContext (). Override Pending Transition (0,0) to cancel the default activity to replace the animation, so as to achieve the purpose of dark warehouse display. You should have seen that there is also a BackView here. This BackView is actually a copy of the preView. We add the BackView to the bottom of the curContentView and cover the white bottom. Otherwise, there will be a white screen flashing after the animation is finish ed.

Another special case is that the user slides and then clicks the return key to return. If we don't do any processing in this case, we will find that because the user slides, we move the preView, so when the user clicks the return key to return, there is no linkage settings. This will cause preView to be offset, so we need to do this. Modify the preView to its original position in the function so that it can be avoided and the user suddenly changes his mind to use the return key to return.

//Used to prevent white screen flickering
class BackView extends View{

    private View mView;
    public BackView(Context context) {
        super(context);
    }

    public void cacheView(View view){
        mView=view;
        invalidate();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        if(mView!=null){
            mView.draw(canvas);
            mView=null;
        }
    }
}

And the shadow class that has been selling to the present day is actually a Drawable that we have drawn ourselves.

class ShadowView extends View{

    private Drawable mDrawable;

    public ShadowView(Context context) {
        super(context);
        int[] colors=new int[]{0x00000000, 0x17000000, 0x43000000};
        mDrawable=new GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT,colors);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        mDrawable.setBounds(0,0,getMeasuredWidth(),getMeasuredHeight());
        mDrawable.draw(canvas);
    }
}

In this way, we have completed all our code.

To configure

Because we have rewritten the Application class, we need to use our own Application in Android Manifest to record Activities on the stack.

For activities that can achieve sliding returns, we just inherit SwipeBackActivity. In fact, there is one thing that can be improved here. If there is a return button, if we finish() directly, there is no sliding effect, and our SwipeBack Activity actually has finish(), so we can use it to achieve it. If we click the return button, we can perform the function in SwipeBack Activity to find out indirectly.

this.getTouchHelper().startSlide();
                this.getTouchHelper().startAnimating(true, 0);

Just one call. In that case, it will be a great success.

Reference in this article http://www.jianshu.com/p/b194b0234d2c

Posted by jeff_lawik on Wed, 10 Apr 2019 17:06:32 -0700