Introduction:

When the system control can not meet our needs, this time we need to customize the control, according to our needs to customize a control to meet our needs. A user familiar with the control is a good control, if the blindly pursuit of cool effect, will let the user feel flashy.

The main content

  • Learn about custom controls
  • Extend existing controls
  • Creating a composite control
  • Rewrite the View to implement the new control
  • Custom ViewGroup

The specific content

Custom control can be to expand the existing control, create composite control, rewrite View to achieve a new control, according to different needs to choose the best way to customize control.

Learn about custom controls

When customizing a View, we often override the onDraw() method to draw the View’s display content. If the View also needs to use the wrap_content property, then the onMeasure() method must also be overridden. In addition, you can set new attribute configuration values by customizing the attrs attribute. Some of the most important callback methods in a View are:

  • OnFinishInflate () : callback after loading a component from XML.
  • OnSizeChanged () : Callback when component size changes.
  • OnMeasure () : calls back the method to measure.
  • OnLayout () : Calls back to this method to determine the location of the display.
  • OnTouchEvent () : Callback to listen for touch events.

Of course, when you create a custom View, you don’t need to override all of the methods, just the callback methods for certain conditions. This demonstrates the flexibility of the Android control architecture. In general, there are three ways to implement custom controls:

  • Extend existing controls
  • Creating a composite control
  • Rewrite the View to implement the new control

Extend existing controls

This is a very important custom View method, which can be extended on the basis of the native control, add new functions, modify the display UI, etc. In general, we can extend native control behavior in the onDraw() method. Taking a TextView as an example, Canvas object is used to draw the image, and then the drawing mechanism of Android is used to draw a more complex and rich image. For example, you can use LinearGradient Shader and Matrix to achieve a dynamic text flashing effect, as shown in the figure below.

To achieve this effect, take full advantage of the Shader renderer for Paint objects in Android. Set an ever-changing LinearGradient and use the Paint object with that property to draw the text to display. First, do some object initialization in the onSizeChanged() method and set a LinearGradient gradient renderer based on the width of the View as follows:

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh){
    super.onSizeChanged(w, h, oldw, oldh);
    if(mViewWidth  == 0) {
        mViewWidth = getMeasuredWidth();
        if(mViewWidth > 0) {
            mPaint = getPaint();
            // Create the mLinearGradient gradient renderer and fill it with brush mPaint
            mLinearGradient = new LinearGradient(0.0, mViewWidth, 0.new int[] {Color.BLUE, 0xffffffff, Color.BLUE}, 
                    null, Shader.TileMode.CLAMP);
            mPaint.setShader(mLinearGradient);
            mGradientMatrix = newMatrix(); }}}Copy the code

The key is to use the getPaint() method to get the Paint object that is currently drawing the TextView, and set the LinearGradient property to that Paint object, which the native TextView doesn’t have. Finally, in the onDraw() method, we use a matrix to shift the gradient to create a dynamic cooling effect as we draw text, as shown below:

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    if(mGradientMatrix ! =null) {
        mTranslate += mViewWidth / 5;
        if(mTranslate > 2 * mViewWidth) {
            mTranslate = -mViewWidth;
        }
        mGradientMatrix.setTranslate(mTranslate, 0);  // Set the displacement
        mLinearGradient.setLocalMatrix(mGradientMatrix);
        postInvalidateDelayed(100);  // Delay refresh}}Copy the code

This completes the extension of the TextView to create a TextView with dynamic text flashing.

Creating a composite control

Creating composite controls is a great way to create a collection of controls that are specifically reusable. This approach typically involves inheriting an appropriate ViewGroup and adding controls with specified functions to compose new composite controls. Controls created in this way are typically assigned configurable properties to make them more extensible. Let’s use a generic TopBar as an example to show you how to create composite controls. The effect is shown below.

Start by creating a New TopBar class that inherits from RelativeLayout.

public class TopBar extends RelativeLayout {

    public TopBar(Context context) {
        this.TopBar(context, null);
    }

    public TopBar(Context context, AttributeSet attrs) {
        super(context, attrs);
        // The initialization method
        // Initialize the property
        initAttr(context, attrs)
        // Initialize the layout
        initView(context);
        // Initializing eventsinitEvent(); }}Copy the code
Custom properties

Providing customizable attributes for a View is as simple as creating an attrs.xml attribute definition file in the VALUES directory of the RES resource directory and defining the corresponding attributes in that file using the following code.


      

<resources>

    <declare-styleable name="TopBar">
        <! Define title text, size, color -->
        <attr name="title" format="string" />
        <attr name="titleTextSize" format="dimension" />
        <attr name="titleTextColor" format="color" />
        <! Define left text, size, color, background -->
        <attr name="leftTextColor" format="color" />
        <attr name="leftTextSize" format="dimension" />
        <! -- indicates that the background can be a color or a reference.
        <attr name="leftBackground" format="reference|color" />
        <attr name="leftText" format="string" />
        <! Define right text, size, color, background -->
        <attr name="rightTextColor" format="color" />
        <attr name="rightTextSize" format="dimension"/>
        <attr name="rightBackground" format="reference|color" />
        <attr name="rightText" format="string" />
    </declare-styleable>

</resources>
Copy the code

We through the label statement in the code to use custom attributes, and through the name of the name attribute to determine the reference, through the custom tag to declare the specific properties, such as defines the content of the title words here, size, color, the background of the left and right buttons, text, color and other properties, and through the format attribute to specify the type of the property. It is important to note that some properties can be color properties as well as reference properties. Background such as buttons, so use “|” to separate different attribute – “reference | color”. Once the properties are determined, you can create a custom control, TopBar, and have it inherit from the ViewGroup to combine the necessary controls. For simplicity, we’ll inherit from RelativeLayout. In the constructor, we get those attributes that we have customized in the XML layout file as we use those provided by the system by using the code shown below.

TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);
Copy the code

TypedArray data structures are provided to retrieve the custom set of attributes, and the TopBar of the styleable referenced below is the name we specified in the XML. These defined attribute values can then be retrieved using methods such as getString(), getColor(), etc. on the TypeArray object, as shown below.

private int mLeftTextColor;
private Drawable mLeftBackground;
private String mLeftText;
private float mLeftTextSize;

private int mRightTextColor;
private Drawable mRightBackground;
private String mRightTextSize;
private float mRightTextSize;

private String mTitleText;
private float mTitleTextSize;
private int mTitleTextColor;

private void initAttr(Context context, AttributeSet attrs) {
    // Use this method to store the values of all the declare-styleable attributes you defined in attrs.xml into TypedArray.
    TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.TopBar);
    // Assign the property to be set by fetching the corresponding value from TypedArray
    mLeftTextColor = ta.getColor(R.styleable.TopBar_leftTextColor, 0);
    mLeftBackground = ta.getDrawable(R.styleable.TopBar_leftBackground);
    mLeftText = ta.getString(R.styleable.TopBar_leftText);
    mLeftTextSize = typed.getDimension(R.styleable.TitleBar_leftTextSize, 20);

    mRightTextColor = ta.getColor(R.styleable.TopBar_rightTextColor, 0);
    mRightBackground = ta.getDrawable(R.styleable.TopBar_rightBackground);
    mRightText = ta.getString(R.styleable.TopBar_rightText);
    mRightTextSize = typed.getDimension(R.styleable.TitleBar_rightTextSize, 20);

    mTitleText = ta.getString(R.styleable.TopBar_titleText);
    mTitleTextSize = ta.getDimension(R.styleable.TopBar_titleTextSize, 10);
    mTitleTextColor = ta.getColor(R.styleable.TopBar_titleTextColor, 0);

// After retrieving the TypedArray values, recyle() is used to avoid errors in the creation process
ta.recycle();
}
Copy the code

Note that the recyle() method of TypedArray needs to be used to recycle the resource after all the property values are obtained.

The combination control

Next, we can begin to compose the controls. TopBar consists of three controls, mLeftButton on the left, mRightButton on the right and mTitleView in the middle. By dynamically adding controls, add the three controls to the defined TopBar template using the addView() method and set them to the specific property values we obtained earlier, such as title text, color, size, and so on, as shown below.

private TextView mTitleView;
private Button mLeftButton;
private Button mRightButton;

private RelativeLayout.LayoutParams mLeftParams;
private RelativeLayout.LayoutParams mRightParams;
private RelativeLayout.LayoutParams mTitleParams;

private void initView(Context context) {
    mTitleView = new TextView(context);
    mLeftButton = new Button(context);
    mRightButton = new Button(context);

    // Assign a value to the created component. The value comes from the corresponding attribute assigned in the referenced XML file
    mTitleView.setText(mTitleText);
    mTitleView.setTextSize(mTitleTextSize);
    mTitleView.setTextColor(mTitleTextColor);
    mTitleView.setGravity(Gravity.CENTER);

    mLeftButton.setText(mLeftText);
    mLeftButton.setTextColor(mLeftTextColor);
    mLeftButton.setBackgroundDrawable(mLeftBackground);
    mLeftButton.setTextSize(mLeftTextSize);

    mRightButton.setText(mRightText);
    mRightButton.setTextSize(mRightTextSize);
    mRightButton.setBackgroundDrawable(mRightBackground);
    mRightButton.setTextColor(mRightTextColor);

    // Set the layout element for the component element
    // Set the layout_width and layout_height attributes for the layout
    mLeftParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
    // This method means that the properties of the node set must be associated with other sibling nodes or the property value must be a Boolean value.
    mLeftParams.addRule(RelativeLayout.ALIGN_PARENT_LEFT, RelativeLayout.TRUE);
    // Dynamically add components
    addView(mLeftButton, mLeftParams);

    mRightParams= new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
    mRightParams.addRule(RelativeLayout.ALIGN_PARENT_RIGHT, RelativeLayout.TRUE);
    addView(mRightButton, mRightParams);

    mTitleParams= new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
    mTitleParams.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
    addView(mTitleView, mTitleParams);
}
Copy the code

Since these are UI templates, each caller needs to implement the buttons differently. Therefore, instead of implementing the logic directly in the UI template, the implementation logic can be handed to the caller through the idea of interface callbacks. The implementation process is shown below.

  • Defines the interface

Define an interface for left and right button clicks and create two methods, one for left and right button clicks, as shown below.

// Define an interface object inside the class to implement the callback mechanism, regardless of how to implement, the implementation of the caller to create
public interface OnClickListener{
    // Left button click event
    void leftClick(a);
    // Right button click event
    void rightClick(a);
    }
Copy the code
  • Expose the interface to the caller

In the template method, add a click event to the left and right buttons, but instead of implementing the specific logic, invoke the corresponding click method in the interface, as shown below.

// Create an interface object
private OnClickListener mListener;

// Expose a method to the caller to register the interface and get the callback's implementation of the interface method through the interface
public void setOnClickListener(OnClickListener listener) {
    this.mListener = listener;
}

private void initEvent(a){
    // The button click event does not require a specific implementation, just call the interface method, the callback will have a specific implementation
    mLeftButton.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) { mListener.leftClick(); }}); mRightButton.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) { mListener.rightClick(); }}); }Copy the code
  • Implement the interface callback

In the caller’s code, the caller needs to implement such an interface, complete the methods in the interface, determine the implementation logic, and use the methods exposed in step 2 to pass in the object of the interface to complete the callback. In general, you can implement methods in the interface in the form of anonymous inner classes, as shown below.

mTopBar.setOnClickListener(new TopBar.OnClickListener() {
    @Override
    public void leftClick(a) {
        // Click the left button
    }

    @Override
    public void rightClick(a) {
        // Click the right button}}Copy the code

Add logic code to each method after clicking the left and right buttons.

In addition to dynamically controlling the UI template through interface callbacks, you can also use public methods to dynamically modify the UI in the UI template, further improving the customizability of the template, as shown below.

public static final int LEFT = 1;
public static final int RIGHT = 2;

/** * Sets whether the button display is visible or not@paramView tags view *@param* / public void setVisable(int view, int Visible){switch(view) {case LEFT: mLeftButton.setVisibility(visible); break; case RIGHT: mRightButton.setVisibility(visible); break; }}Copy the code

Using the code above, the caller can dynamically control the display of the button based on the parameters after calling the method from the TopBar object, as shown below.

// Control the state of components on TopBar
mTopBar.setVisable(TopBar.LEFT, View.VISIBLE);
mTopBar.setVisable(TopBar.RIGHT, View.GONE);
Copy the code
Referencing UI templates

The last step, of course, is to reference the UI template where it needs to be used, before specifying the namespace to reference the third-party controls. In the layout file, you can see the following line of code.

xmlns:android="http://schemas.android.com/apk/res/android"
Copy the code

This line of code is specifying the referenced namespace XMLNS, or XML namespace. The namespace is specified as “Android”, so “Android:” can be used to refer to android system properties when using system properties. Also, if you want to use custom properties, you need to create your own namespace. In Android Stuido, third-party controls refer to namespaces using the following code.

xmlns:app="http://schemas.android.com/apk/res/res-auto"
Copy the code

Here we will introduce a third-party control with a namespace named app that can be referenced later when using custom properties in the XML file, as shown below.

<com.example.demo.TopBar
        android:id="@+id/tb"
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:layout_alignParentBottom="true"
        app:leftBackGround="#ff000000"
        app:leftText="Back"
        app:leftTextColor="#ffff6734"
        app:leftTextSize="25dp" 
        app:rightText="More"
        app:rightTextSize="25dp"
        app:rightTextColor="#ff123456"
        app:title="Custom title"
        app:titleTextColor="#ff654321"/>
Copy the code

The main difference between using a custom View and a system-native View is that you need to specify the full package name when declaring controls, and you need to use the custom XMLNS name when referencing custom properties.

Rewrite the View to implement a completely new control

When the Android system native controls can not meet our needs, we can completely create a new custom View to achieve the desired function. To create a custom View, the difficulty is to draw controls and achieve interaction, which is also one of the criteria for evaluating the merits of a custom View. At the same time, we need to inherit the View class, rewrite its onDraw(), onMeasure() and other methods to achieve the drawing logic, while rewriting onTouchEvent() and other touch events to achieve the interaction logic. You can also implement composite controls by introducing custom properties to enrich the customizability of custom views.

Arc display diagram

In many PPT templates, there are often scale maps, as shown in the figure below.

This custom View is actually divided into three parts, namely the circle in the middle, the text displayed in the middle and the arc of the outer circle. For simplicity, we set the drawing length of the View directly to the width of the screen. First set the parameters to draw the three shapes during initialization, and then draw them in the onDraw() method. The code is shown below.

public class ScaleMap extends View {

    private int mMeasureHeigth;// Control height
    private int mMeasureWidth;// Control width
    / / round
    private Paint mCirclePaint;
    private float mCircleXY;// Center coordinates
    private float mRadius;// Circle radius
    / / arc
    private Paint mArcPaint;
    private RectF mArcRectF;// The outer tangent rectangle of the arc
    private float mSweepAngle;// Angle of the arc
    private float mSweepValue;
    / / text
    private Paint mTextPaint;
    private String mShowText;// Text content
    private float mShowTextSize;// Text size

    public ScaleMap(Context context) {
        this(context, null);
    }

    public ScaleMap(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ScaleMap(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }
    
    // If you do not use the following parameters, you do not need to refactor the following parameters, just write the contents of the first constructor. The parent class automatically executes the following constructor
    public ScaleMap(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        // Initialize the operation
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        mMeasureWidth = MeasureSpec.getSize(widthMeasureSpec);// Get the control width
        mMeasureHeigth = MeasureSpec.getSize(heightMeasureSpec);// Get the control height
        setMeasuredDimension(mMeasureWidth, mMeasureHeigth);

        initPaint();  // The brush uses the width and height so initialize the brush here
    }

    /** * ready brushes, */
    private void initPaint(a) {
        float length = Math.min(mMeasureWidth,mMeasureHeigth);
        // Circle code
        mCircleXY = length / 2;// Determine the center coordinates
        mRadius = (float) (length * 0.5 / 2);// Determine the radius
        mCirclePaint = new Paint();
        mCirclePaint.setAntiAlias(true);/ / to serrate
        mCirclePaint.setColor(getResources().getColor(android.R.color.holo_green_dark));

        // Curve, need to specify the ellipse of the enclosing rectangle
        / / rectangle
        mArcRectF = new RectF((float) (length * 0.1), (float) (length * 0.1), (float)(length * 0.9), (float) (length * 0.9));
        mSweepAngle = (mSweepValue / 100f) * 360f;
        mArcPaint = new Paint();
        mArcPaint.setColor(getResources().getColor(android.R.color.holo_blue_bright));
        mArcPaint.setStrokeWidth((float) (length * 0.1));// Arc width
        mArcPaint.setStyle(Style.STROKE);/ / arc
        // Text, just set the starting position of the text
        mShowText = "Android Skill";
        mShowTextSize = 50;
        mTextPaint = new Paint();
        mTextPaint.setTextSize(mShowTextSize);
        mTextPaint.setTextAlign(Paint.Align.CENTER);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        / / draw circle
        canvas.drawCircle(mCircleXY, mCircleXY, mRadius, mCirclePaint);
        // Draw the arc, draw counterclockwise, Angle with
        canvas.drawArc(mArcRectF, 90, mSweepAngle, false, mArcPaint);
        // Draw text
        canvas.drawText(mShowText, 0, mShowText.length(), mCircleXY, mCircleXY + mShowTextSize / 4, mTextPaint);
    }

    // Let the caller set different state values to make the arc radian change
    public void setSweepValue(float sweepValue) {
        if(sweepValue ! =0) {
            mSweepValue = sweepValue;
        } else {
            mSweepValue = 25;
        }
        // This method refreshes the UI
        this.invalidate(); }}Copy the code

This can be done by calling this.invalidate() after the UI has been modified.

Audio bar chart

Just to demonstrate the use of a custom View, unreal listening for audio input, random simulation of some numbers. First, take a look at the resulting renderings, as shown below.

To achieve this effect, draw rectangles with a slight offset between them. To achieve this dynamic effect, simply call the invalidate() method in the onDraw() method to tell the View to redraw. If you redraw directly, it will refresh too quickly, so you can use postInvalidateDelayed(300) for delayed redraw. The code is shown below.

this.invalidate();
this.postInvalidateDelayed(300);
Copy the code

And you can add a LinearGradient effect to the painted object. The code is shown below.

private int mWidth;// Width of the control
private int mRectWidth;// Width of rectangle
private int mRectHeight;// Height of the rectangle
private Paint mPaint;
private int mRectCount;// Number of rectangles
private int offset = 5;/ / migration
private double mRandom;
private LinearGradient lg;/ / the gradient

public ScaleMap(Context context) {
    this(context, null);
}

public ScaleMap(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
    initPaint();  // These need to be set here because of the gradient effect
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // Set width and height
    setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.getSize(heightMeasureSpec));
}

// Initialize the brush
private void initPaint(a) {
    mPaint = new Paint();
    mPaint.setColor(Color.GREEN);
    mPaint.setStyle(Paint.Style.FILL);
    mRectCount = 12;
}

// Override the onDraw method
@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    for (int i = 0; i < mRectCount; i++) {
        mRandom = Math.random();
        float currentHeight = (int) (mRectHeight * mRandom);
        canvas.drawRect((float) (mWidth * 0.4 / 2 + mRectWidth * i + offset * i), currentHeight,
        (float) (mWidth * 0.4 / 2 + mRectWidth * (i + 1) + offset * i), mRectHeight, mPaint);
    }
    postInvalidateDelayed(300);
}

// Override onSizeChanged to add gradient to the brush
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    mWidth = getWidth();
    mRectHeight = getHeight();
    mRectWidth = (int) (mWidth * 0.6 / mRectCount);
    lg = new LinearGradient(0.0, mRectWidth, mRectHeight, Color.GREEN, Color.BLUE, TileMode.CLAMP);
    mPaint.setShader(lg);
}

Copy the code

Customize the View step by step, starting from the most basic effects, gradually add functionality, draw more complex effects.

Custom ViewGroup

The purpose of ViewGroup is to manage its sub-views and add display and response rules for its sub-views. Therefore, custom viewgroups usually override the onMeasure() method to measure the child View, the onLayout() method to determine the location of the child View, and the onTouchEvent() method to add response events. It is ready to implement a custom ViewGroup similar to the Android native control ScrollView. The custom ViewGroup realizes the function of ScrollView sliding up and down, but adds a sticky effect in the sliding process. That is, when a child View slides up more than a certain distance, Release your finger and it will automatically slide up to display the next child View. Similarly, if you slide less than a certain distance, release your finger and it will automatically slide back to its starting position. The renderings are shown below.

First, let the custom ViewGroup implement scrollView-like functionality. Before a ViewGroup can scroll, its child views need to be placed. Use traversal to tell the child View to measure itself, as shown below.

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int count = getChildCount();  // Returns the number of child views
    for (int i = 0; i < count; ++i) {
        View childView = getChildAt(i);  // Get the child View
        measureChild(childView, widthMeasureSpec, heightMeasureSpec);  // Call the measurement method of the child View}}Copy the code

Next, we need to set the position of the child View. Let each child View display a full screen, so that when sliding, you can better achieve the latter effect. Before placing child views, you need to determine the height of the entire ViewGroup. In this example, since each sub-view occupies the height of a screen, the height of the entire ViewGroup is the number of sub-views multiplied by the height of the screen. We use the following code to determine the height of the entire ViewGroup.

// Set the ViewGroup height
MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
mlp.height = mScreenHeight * childCount;
setLayoutParams(mlp);
Copy the code

After obtaining the height of the entire ViewGroup, you can set the position of each child View by iterating through it by calling the layout() method of the child View and passing in the specific position as an argument, as shown in the code below.

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int childCount = getChildCount();
    // Set the ViewGroup height
    MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
    mlp.height = mScreenHeight * childCount;
    setLayoutParams(mlp);
    for (int i = 0; i < childCount; ++i) {
        View child = getChildAt(i);
        if(child.getVisibility() ! = View.GONE) { child.layout(l, i * mScreenHeight, r, (i +1) * mScreenHeight); }}}Copy the code

In the code, the main task is to modify the top and bottom attributes of each child View so that they can be arranged in order. By following the steps above, you can place the child View into the ViewGroup. But the ViewGroup can’t respond to any touch events yet, nor can it slide, so we need to rewrite the onTouchEvent() method to add response events to the ViewGroup. To add a slide event to a ViewGroup, you can usually use the scrollBy() method to assist the slide. In the ACTION_MOVE event of onTouchEvent(), just use the scrollBy(0,dy) method to make all the child views in the ViewGroup scroll dy as the finger slides. There are many ways to calculate dy, and the following code provides an idea.

case MotionEvent.ACTION_DOWN:
    mLastY = y;
    break;
case MotionEvent.ACTION_MOVE:
    if(! mScroller.isFinished()) { mScroller.abortAnimation(); }int dy = mLastY - y;
    if (getScrollY() < 0 || getScrollY() > getHeight() - mScreenHeight) {
        dy = 0;
    }
    scrollBy(0, dy);
    mLastY = y;
    break;
Copy the code

Follow the above method to achieve a scrollview-like effect. Of course, the system’s native ScrollView has much larger features, such as sliding inertia effects, which can be added later as part of an iterative process of controls. Finally, let’s implement the stickiness effect of this custom ViewGroup. To achieve the sticky effect of the ViewGroup after the finger leaves, we naturally think of the ACTION_UP event of onTouchEvent() and the Scroller class. In the ACTION_UP event, determine the sliding distance of the finger. If it exceeds a certain distance, use the Scroller class to smooth the movement to the next subview. If it is less than a certain distance, it is rolled back to its original location, as shown below.

@Override
public boolean onTouchEvent(MotionEvent event) {
    int y = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            // Record the start of the touch
            mStart = getScrollY();
            mLastY = y;
            break;
        case MotionEvent.ACTION_MOVE:
            if(! mScroller.isFinished()) { mScroller.abortAnimation(); }int dy = mLastY - y;
            if (getScrollY() < 0 || getScrollY() > getHeight() - mScreenHeight) {
                dy = 0;
            }
            scrollBy(0, dy);
            mLastY = y;
            break;
        case MotionEvent.ACTION_UP:
            // Record the end of the touch
            mEnd = getScrollY();
            int dScrollY = mEnd - mStart;
            if (dScrollY > 0) {
                if (dScrollY < mScreenHeight / 3) {
                    mScroller.startScroll(
                            0, getScrollY(),
                            0, -dScrollY);
                } else {
                    mScroller.startScroll(
                            0, getScrollY(),
                            0, mScreenHeight - dScrollY); }}else {
                if (-dScrollY < mScreenHeight / 3) {
                    mScroller.startScroll(
                            0, getScrollY(),
                            0, -dScrollY);
                } else {
                    mScroller.startScroll(
                            0, getScrollY(),
                            0, -mScreenHeight - dScrollY); }}break;
    }
    postInvalidate();
    return true;
}
Copy the code

Finally, of course, don’t forget to add the code for computeScroll(), as shown below.

// Called by the superview to request the subview to redraw according to the offset values mScrollX,mScrollY
@Override
public void computeScroll(a) {
    super.computeScroll();
    if(mScroller.computeScrollOffset()){
        scrollTo(0,mScroller.getCurrY()); postInvalidate(); }}Copy the code

The Android framework provides computeScroll() methods to control this process in order to make it easy to control the slide control. This method is called in the draw() procedure when the View is drawn. So, with the Scroller instance, we can get the current offset coordinate and manually offset the View/ViewGroup to that point.