Custom ViewGroup - implement custom ViewPager

Keywords: Android Java xml encoding

ViewGroup and View

1. View Group is a container that can hold views. It is responsible for measuring the width and height of subviews or sub controls, and determining the location of subviews or sub controls. Common methods are:

  • onMesure(): measures the width and height of a subview or child control, and sets its own width and height.
  • onLayout(): get the number of child views by getChildCount(), get all child views by getChildAt, respectively call layout(int l, int t, int r, int b) to determine the placement of each child view.
  • onSizeChanged(): executed after onMeasure(), onSizeChange will only be executed if the size changes.
  • onDraw(): it will not be triggered by default and needs to be triggered manually.

2. View draws its own shape in the area designated by ViewGroup according to the measurement mode and the recommended width and height given by ViewGroup. Common methods are:

  • onMesure(): test the view size, mainly dealing with the situation of wrap ﹣ content;
  • onDraw(): draws a graph in the area specified by the parent view.

Customize the ViewPager

Let's implement a custom ViewPager for rotating pictures.
1. Inherit the ViewGroup and write a method to add picture data, which is convenient to add pictures to the ViewGroup container

package com.wong.support;

import android.content.Context;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.widget.ImageView;

import java.util.List;

public class WonViewPager extends ViewGroup {

    /*To rotate a picture*/
    private List<Integer> images;

    public WonViewPager(Context context) {
        super(context);
    }

    public WonViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public WonViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public WonViewPager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

    }
    /*Batch set up rotation pictures*/
    public void setImages(List<Integer> images) {
        this.images = images;
        updateViews();
    }
    /*Add subviews to the ViewGroup container*/
    private void updateViews(){
        for(int i = 0; i < images.size(); i++){
            ImageView iv = new ImageView(getContext());
            iv.setBackgroundResource(images.get(i));
            this.addView(iv);
        }
    }
}

2. Rewrite the onLayout() method, get all the sub views, call the layout() method respectively, arrange them according to the following figure, and determine their respective positions.
First, let's know the location of the picture:

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        for(int i = 0; i < childCount; i++){
            View childView = getChildAt(i);
            childView.layout(i*getWidth(),t,(i+1)*getWidth(),b);
        }
    }

3. Create gesture recognizer Gesturedetector to complete the function of sliding subview.

(1) Create a gesture recognizer Gesturedetector
Gesture recognizer can recognize many kinds of gestures and events through MotionEvent. When a specific action event occurs, the onTouchEvent(MotionEvent) of gesture recognizer Gesturedetector will be called. In this method, the user will be notified of the specific action event by calling the callback method defined by onggesturelistener.

 GestureDetector mGestureDetector = new GestureDetector(getContext(),new GestureDetector.OnGestureListener(){
        @Override
        public boolean onDown(MotionEvent e) {
            return false;
        }
        @Override
        public void onShowPress(MotionEvent e) {
        }
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            return false;
        }
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            //Relative sliding: view will follow the sliding distance in X direction
            scrollBy((int) distanceX, 0);
            return false;
        }
        @Override
        public void onLongPress(MotionEvent e) {
        }
        @Override
        public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
            return false;
        }
    });

(2) Rewrite the onTouchEvent() method in the wonviewpager, pass the touch event to the gesture recognizer, and return true to let the control consume the event.

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //Passing touch events to gesture recognizer
        mGestureDetector.onTouchEvent(event);
        return true;
    }

2, application
activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <com.wong.support.WonViewPager
        android:id="@+id/wvp"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

MainActivity.java

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        WonViewPager wonViewPager = findViewById(R.id.wvp);
        List<Integer> list = new ArrayList<>();
        list.add(R.drawable.a);
        list.add(R.drawable.b);
        list.add(R.drawable.c);
        list.add(R.drawable.d);
        wonViewPager.setImages(list);
    }
}

Effect:

The function of sliding pictures by fingers is realized above.

4. Optimize: handle the boundary situation and move smoothly to the specified position.

  • Boundary case processing: when the finger is released, if the sliding offset distance exceeds 1 / 2 of the picture, it will automatically switch to the next picture, otherwise it will rebound to the initial position. Here we need to handle touch events in onTouchEvent():
  /*Record the sequence number of the current view*/
    private int position;
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //Passing touch events to gesture recognizer
        mGestureDetector.onTouchEvent(event);
        switch (event.getAction()){
            /*Press down*/
            case MotionEvent.ACTION_DOWN:

                break;
            /*Move, between action? Down and action? Up*/
            case MotionEvent.ACTION_MOVE:
                /*Return to the left scrolling position of the part being displayed in the view (i.e. return to the left position of the scrolling view)*/
                int scrollX = getScrollX();
                /*Plus half of the parent view*/
                int totalWidth = scrollX + getWidth()/2;
                /*Calculate the sequence number of the next view after half of the view*/
                position = totalWidth / getWidth();
                 /*Calculate the sequence number of the next view after half of the view*/
                position = totalWidth / getWidth();
                /* scrollX >= getWidth() * (images.size() - 1)Note is the last one, then we can not let it out of the boundary, otherwise it can slip out of the boundary*/
                if (scrollX >= getWidth() * (images.size() - 1)) {
                    position = images.size() - 1;
                }
                /*scrollX < 0 It means that the left side of the first view is to the right, and the distance from the left side of the parent view is blank*/
                if (scrollX <= 0) {
                    position = 0;
                }

                break;
            /*Raise your finger*/
            case MotionEvent.ACTION_UP:
            	/*Slide to the specified view*/
                scrollTo(position*getWidth(),0);
                break;

        }
        return true;
    }

Effect:

  • Move smoothly to the specified position
    scrollTo(position*getWidth(),0) will move directly to the specified position, giving a "sudden" feeling without smooth transition. We can use the startScroll(int startX, int startY, int dx, int dy) method of the Scroller class to realize the smooth scrolling of View.

Step 1: define the Scroller object

private Scroller scroller = new Scroller(getContext());

Step 2: call the startScroll(int startX, int startY, int dx, int dy) method, which will not trigger scrolling, because it finally calls the following method (from android source), which is just collecting process data, and call the invalidate() method to trigger view refresh:

 /**
     * Start scrolling by providing a starting point, the distance to travel,
     * and the duration of the scroll.
     * 
     * @param startX Starting horizontal scroll offset in pixels. Positive
     *        numbers will scroll the content to the left.
     * @param startY Starting vertical scroll offset in pixels. Positive numbers
     *        will scroll the content up.
     * @param dx Horizontal distance to travel. Positive numbers will scroll the
     *        content to the left.
     * @param dy Vertical distance to travel. Positive numbers will scroll the
     *        content up.
     * @param duration Duration of the scroll in milliseconds.
     */
    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        mMode = SCROLL_MODE;
        mFinished = false;
        mDuration = duration;
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
        mStartX = startX;
        mStartY = startY;
        mFinalX = startX + dx;
        mFinalY = startY + dy;
        mDeltaX = dx;
        mDeltaY = dy;
        mDurationReciprocal = 1.0f / (float) mDuration;
    }

Step 3: Rewrite computeScroll() to complete the actual scrolling

 @Override
    public void computeScroll() {
        super.computeScroll();
        if(scroller.computeScrollOffset()){
            scrollTo(scroller.getCurrX(),0);
            postInvalidate();
        }
    }

Modified code:

    /*Record the sequence number of the current view*/
    private int position;
    private Scroller scroller = new Scroller(getContext());
    private int scrollX;
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //Passing touch events to gesture recognizer
        mGestureDetector.onTouchEvent(event);
        switch (event.getAction()){
            /*Press down*/
            case MotionEvent.ACTION_DOWN:

                break;
            /*Move, between action? Down and action? Up*/
            case MotionEvent.ACTION_MOVE:
                /*Return to the left scrolling position of the part being displayed in the view (i.e. return to the left position of the scrolling view)*/
                scrollX = getScrollX();
                /*Plus half of the parent view*/
                int totalWidth = scrollX + getWidth()/2;
                /*Calculate the sequence number of the next view after half of the view*/
                position = totalWidth / getWidth();
                 /*Calculate the sequence number of the next view after half of the view*/
                position = totalWidth / getWidth();
                /* scrollX >= getWidth() * (images.size() - 1)The explanation is the last one, then we can't let it out of the boundary, otherwise it can slide out of the boundary*/
                if (scrollX >= getWidth() * (images.size() - 1)) {
                    position = images.size() - 1;
                }
                /*scrollX < 0 It means that the left side of the first view is to the right, and the distance from the left side of the parent view is blank*/
                if (scrollX <= 0) {
                    position = 0;
                }

                break;
            /*Raise your finger*/
            case MotionEvent.ACTION_UP:
                /*Slide to specified position*/
//                scrollTo(position*getWidth(),0);
                /*Smooth move to specified position*/
                scroller.startScroll(scrollX,0,-(scrollX-position*getWidth()),0);
                /*Trigger view update from UI thread*/
                invalidate();
                break;

        }
        return true;
    }

    /**
     * Called by a parent to request that a child update its values for mScrollX
     * and mScrollY if necessary. This will typically be done if the child is
     * animating a scroll using a {@link android.widget.Scroller Scroller}
     * object.
     */
    @Override
    public void computeScroll() {
        super.computeScroll();
        if(scroller.computeScrollOffset()){
            /**
             * Every time the x-axis changes, it will move a little bit. To keep changing, you need to call postInvalidate() to refresh the view continuously,
             * The above invalidate() method is only responsible for the first trigger of computeScroll() call, and the rest is triggered by postInvalidate()
             */
            scrollTo(scroller.getCurrX(),0);
            /*Trigger view update from non UI thread, only call*/
            postInvalidate();
        }
    }

Effect:

5. Optimization: do not slide right until the last screen, and do not slide left until the first screen
In our previous examples, we will find that the first screen will slide to the right, and a blank will appear, and the last screen will also show a similar situation. Because we started with the movement in the gesture recognizer, we could do an article in the gesture recognizer GestureDetector.
Train of thought:
1. By subtracting the end point and the start point of the path crossed by fingers, the direction is judged according to the positive and negative;
2. If it is positive, it means to draw to the right, and then judge whether it is the first screen. If it is, it will not scroll;
3. If it is negative, it means to draw to the left, then judge whether it is the most screen, and if so, do not scroll;
The modified GestureDetector code is as follows:

GestureDetector mGestureDetector = new GestureDetector(getContext(), new GestureDetector.OnGestureListener() {

        @Override
        public boolean onDown(MotionEvent e) {
            return false;
        }
        @Override
        public void onShowPress(MotionEvent e) {}
        @Override
        public boolean onSingleTapUp(MotionEvent e) {
            return false;
        }
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            int startX = 0;
            switch (e1.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    startX = (int) e1.getX();
                    break;
            }
            boolean noScroll = false;
            switch (e2.getAction()) {
                case MotionEvent.ACTION_MOVE:
                    int endX = (int) e2.getX();
                    int dx = endX - startX;
                    if (dx < 0) {
                        if (scrollX >= getWidth() * (images.size() - 1)) {
                            noScroll = true;
                        }
                    }
                    if (dx > 0) {
                        if (scrollX <= 0) {
                            noScroll = true;
                        }
                    }
                    break;
                default:
                    break;
            }
            if(!noScroll) {
                scrollBy((int) distanceX, 0);
            }
            return false;
        }

Effect:

There are so many custom viewpagers. Thanks for watching!

Please refer to: demo

306 original articles published, 70 praised, 50000 visited+
Private letter follow

Posted by GroundZeroStudio on Tue, 21 Jan 2020 03:06:58 -0800