View drawing series:

  • DecorView and ViewRootImpl for Android View drawing process

  • Android View drawing process Measure process details (1)

  • Android View drawing process Layout and Draw process details (2)

  • Android View event distribution principle analysis

For Android developers, native controls often can not meet the requirements, developers need to customize some controls, therefore, need to understand the implementation principle of custom view. This way, even when you need custom controls, you can do it easily.

Basic knowledge of

Custom View classification

There are several ways to implement a custom View:

type define
Custom composite control Multiple controls are combined into a new control, which is convenient for multiple reuse
Inherit system View control Inherit from TextView and other system controls, expand on the basic functions of system controls
Inheriting the View Do not reuse system control logic, inherit View function definition
Inherit system ViewGroup Inherit from system controls such as LinearLayout, expand on the basic functions of system controls
Inheriting the View ViewGroup Do not reuse system control logic, inherit ViewGroup function definition

It’s getting harder and harder to go from top to bottom, and it’s getting harder and harder to know.

The constructor

When we customize a View, constructors are indispensable, need to rewrite the constructor, there are multiple constructors, at least one of them to rewrite the line. For example, let’s create a new MyTextView:

Public class MyTextView extends View {/** * public MyTextView(context) extends View {/** * public MyTextView(context) extends View { { super(context); } /** * public MyTextView(context context, @Nullable AttributeSet attrs) { super(context, attrs); } /** * is not automatically called, if there is a default style, In the second constructor, call @param context @param attrs @param defStyleAttr public MyTextView(context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } /** * this is only used when API version >21. In the second constructor, call @param Context @param attrs @param defStyleAttr @param defStyleRes */ @requiresAPI (API = Build.VERSION_CODES.LOLLIPOP) public MyTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); }}Copy the code

The function of each constructor is written out in the code.

Custom attributes

Those of you who have written about layout know that the properties of system controls start with Android in XML. For custom views, you can also customize attributes to use in XML.

Android custom attributes can be divided into the following steps:

  1. Customize a View
  2. Write values/attrs.xml, where you write tag elements like styleable and item
  3. View uses custom properties in layout file (note namespace)
  4. Retrieved from TypedArray in the View constructor

LLDB/MyTextView: \

First I introduced MyTextView in activity_main.xml:

<? The XML version = "1.0" encoding = "utf-8"? > <android.support.constraint.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.example.myapplication.MyTextView android:layout_width="100dp" android:layout_height="200dp" app:testAttr="520" app:text="helloWorld" /> </android.support.constraint.ConstraintLayout>Copy the code

Then I add custom attributes to values/attrs.xml:

<? The XML version = "1.0" encoding = "utf-8"? > <resources> <declare-styleable name="test"> <attr name="text" format="string" /> <attr name="testAttr" format="integer" /> </declare-styleable> </resources>Copy the code

Remember from the constructor that the XML layout calls the second constructor, so get the attributes and parse in this constructor:

/** * public MyTextView(context context, @Nullable AttributeSet attrs) { super(context, attrs); TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.test); int textAttr = ta.getInteger(R.styleable.test_testAttr, -1); String text = ta.getString(R.styleable.test_text); Log.d(TAG, "text =" + text + ", textAttr = "+ textAttr); Toast.maketext (context, text + "" + textAttr, toast.length_long).show(); ta.recycle(); }Copy the code

Note that when you refer to a custom attribute, prefix it with name, otherwise it will not be referenced.

Here I want to screenshot the log, but I just don’t show it, so it’s toast.

Of course, you can also customize many other attributes, including color, String, INTEGER, Boolean, flag, and even blend.

Custom composite control

A custom composite control is a combination of multiple controls into a new control, mainly to solve the repeated use of the same type of layout. Like our HeaderView and dailog at the top, we can combine them into a new control.

Let’s look at the use of custom composite controls through a custom MyView1 instance.

XML layout

<? The XML version = "1.0" encoding = "utf-8"? > <merge xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content"> <TextView android:id="@+id/feed_item_com_cont_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:ellipsize="end" android:includeFontPadding="false" android:maxLines="2" android:text="title" /> <TextView android:id="@+id/feed_item_com_cont_desc" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_below="@id/feed_item_com_cont_title" android:ellipsize="end" android:includeFontPadding="false" android:maxLines="2" android:text="desc" /> </merge>Copy the code

Custom View code:

package com.example.myapplication; import android.content.Context; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; import android.widget.RelativeLayout; import android.widget.TextView; Public class MyView1 extends RelativeLayout {/** title */ private TextView mTitle; /** description */ private TextView mDesc; public MyView1(Context context) { this(context, null); } public MyView1(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MyView1(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initView(context); } /** * @param context */ protected void initView(context context) {View rootView = LayoutInflater.from(getContext()).inflate(R.layout.my_view1, this); mDesc = rootView.findViewById(R.id.feed_item_com_cont_desc); mTitle = rootView.findViewById(R.id.feed_item_com_cont_title); }}Copy the code

Reference the control in the layout

<? The XML version = "1.0" encoding = "utf-8"? > <LinearLayout 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" android:orientation="vertical" tools:context=".MainActivity"> <TextView android:id="@+id/text" android:layout_width="100dp" android:layout_height="100dp" android:clickable="true" android:enabled="false" android:focusable="true" android:text="trsfnjsfksjfnjsdfjksdhfjksdjkfhdsfsdddddddddddddddddddddddddd" /> <com.example.myapplication.MyTextView android:id="@+id/myview" android:layout_width="100dp" android:layout_height="200dp" android:clickable="true" android:enabled="false" android:focusable="true" app:testAttr="520" app:text="helloWorld" /> <com.example.myapplication.MyView1 android:layout_width="wrap_content" android:layout_height="wrap_content" /> </LinearLayout>Copy the code

The final effect is as follows:

 

Inherited system control

Inherit the control system can be divided into inherit View subclass (such as TextView, etc.) and inherit ViewGroup subclass (such as LinearLayout, etc.), according to the different business needs, the implementation of the way will have a relatively large difference. Here is a relatively simple, inherited from the View implementation.

Business requirement: Set the background for the text and add a line in the middle of the layout.

Since this implementation reuses the logic of the system, in most cases we want to reuse the system’s onMeaseur and onLayout processes, so we just need to rewrite the onDraw method. The implementation is very simple, no more words, directly on the code.

package com.example.myapplication; import android.content.Context; import android.graphics.Canvas; import android.graphics.LinearGradient; import android.graphics.Shader; import android.text.TextPaint; import android.util.AttributeSet; import android.widget.TextView; import static android.support.v4.content.ContextCompat.getColor; /** * A gradient line on the left and right sides of the text looks like this: * ———————— text ———————— */ public class DividingLineTextView extends TextView {/** LinearGradient */ private LinearGradient mLinearGradient; /** textPaint */ private TextPaint mPaint; /** private String mText = ""; /** screenWidth */ private int screenWidth; /** private int mStartColor; /** end color */ private int mEndColor; /** private int mTextSize; /** * public DividingLineTextView(Context Context, AttributeSet attrs, int defStyle) {super(Context, attrs, defStyle); mTextSize = getResources().getDimensionPixelSize(R.dimen.text_size); mScreenWidth = getCalculateWidth(getContext()); mStartColor = getColor(getContext(), R.color.colorAccent); mEndColor = getColor(getContext(), R.color.colorPrimary); mLinearGradient = new LinearGradient(0, 0, mScreenWidth, 0, new int[]{mStartColor, mEndColor, mStartColor}, New float[]{0, 0.5f, 1f}, Shader.TileMode.CLAMP); mPaint = new TextPaint(); } public DividingLineTextView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public DividingLineTextView(Context context) { this(context, null); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); mPaint.setAntiAlias(true); mPaint.setTextSize(mTextSize); int len = getTextLength(mText, mPaint); Int sx = mScreenWidth / 2-len / 2; Int ex = mScreenWidth / 2 + len / 2; int height = getMeasuredHeight(); mPaint.setShader(mLinearGradient); DrawLine (mTextSize, height / 2, sx-mTextSize, height / 2, mPaint); drawLine(mTextSize, height / 2, sx-mtextSize, height / 2, mPaint); mPaint.setShader(mLinearGradient); // Draw the right boundary, starting from the right side of the text: DrawLine (ex + mTextSize, height / 2, mscreenWidth-mtextSize, height / 2, mPaint); drawLine(ex + mTextSize, height / 2, mscreenWidth-mtextSize, height / 2, mPaint); } /** * returns the width of the specified text, in px ** @param STR the text to be measured * @param paint the brush to draw this text * @return Returns the width of the text, Unit px */ private int getTextLength(String STR, TextPaint paint) {return (int) paint.measureText(STR); } /** * @param text */ public void update(String text) {mText = text; setText(mText); // Refresh and redraw requestLayout(); } /** * get the width to calculate, take the screen width smaller value, * * @param Context context * @return Screen width */ public static int getCalculateWidth(context context) {int height = context.getResources().getDisplayMetrics().heightPixels; Int Width = context.getResources().getDisplayMetrics().widthPixels; return Math.min(Width, height); }}Copy the code

To draw a View, you need to have some knowledge of Paint(), Canvas, and Path.

Take a look at the reference in the layout:

XML layout

<? The XML version = "1.0" encoding = "utf-8"? > <LinearLayout 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" android:orientation="vertical" tools:context=".MainActivity"> // ...... The same as the previous neglect < com. Example. Myapplication. DividingLineTextView android: id = "@ + id/divide" android: layout_width = "match_parent" android:layout_height="wrap_content" android:gravity="center" /> </LinearLayout>Copy the code

 

Activty contains the following code: \

  protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        DividingLineTextView te = findViewById(R.id.divide);
        te.update("DividingLineTextView");
  }
Copy the code

This is redrawn using the Update () pair, ensuring that the edges are on either side of the text. The visual effects are as follows:

 

Inherit View directly

Direct inheritance of View will be more complex than the previous implementation, the use of this method, there is no need to reuse the logic of the system control, in addition to rewriting onDraw also need to rewrite the onMeasure method.

We use our custom View to draw a square.

First define the constructor and do some initialization

Ublic class RectView extends View{private Paint mPaint = new Paint(); Public RectView(context context) {super(context); init(); } public RectView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } public RectView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { mPaint.setColor(Color.BLUE); }}Copy the code

Rewrite the draw method and draw a square. Note that the padding property is set:

/** * Override protected void onDraw(canvas) {super.ondraw (canvas); Int paddingLeft = getPaddingLeft(); int paddingRight = getPaddingRight(); int paddingTop = getPaddingTop(); int paddingBottom = getPaddingBottom(); Int width = getWidth() -paddingleft -paddingRight; Int height = getHeight() -paddingtop-paddingbottom; // Draw the View, top left coordinate (0+paddingLeft,0+paddingTop), Bottom right coordinates (width+paddingLeft,height+paddingTop) canvas.drawRect(0+paddingLeft,0+paddingTop,width+paddingLeft,height+paddingTop,mPaint); }Copy the code

In the View source code, there is no distinction between AT_MOST and EXACTLY two modes, that is to say, the View is identical in wrap_content and match_parent modes, will be match_parent. Obviously this is different from the View we normally use, so we’re going to rewrite the onMeasure method.

@param widthMeasureSpec @param heightMeasureSpec @override protected void onMeasure(int) widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); If (widthMode == MeasureSpec.AT_MOST &&heightMode == MeasureSpec.AT_MOST) { setMeasuredDimension(300, 300); } else if (widthMode == MeasureSpec.AT_MOST) { setMeasuredDimension(300, heightSize); } else if (heightMode == MeasureSpec.AT_MOST) { setMeasuredDimension(widthSize, 300); }}Copy the code

The final result looks like this:

As you can see, we’re setting wrap_content, but we still have the size at the end.

There are a few things to note when inheriting a View directly:

  1. The padding property is handled in onDraw.
  2. The wrAP_content property is processed during onMeasure.
  3. There must be at least one constructor.

Inherit the ViewGroup

The process of customizing a ViewGroup is a bit more complicated, because in addition to measuring its own size and position, it is also responsible for measuring the parameters of its child views.

Demand for instance

Implement a left-right slider layout similar to Viewpager.

Layout file:

<? The XML version = "1.0" encoding = "utf-8"? > <LinearLayout 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" android:orientation="vertical" tools:context=".MainActivity"> <com.example.myapplication.MyHorizonView android:layout_width="wrap_content" android:background="@color/colorAccent" android:layout_height="400dp"> <ListView android:id="@+id/list1" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorAccent" /> <ListView android:id="@+id/list2" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorPrimary" /> <ListView android:id="@+id/list3" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/colorPrimaryDark" /> </com.example.myapplication.MyHorizonView> <TextView android:id="@+id/text" android:layout_width="100dp" android:layout_height="100dp" android:clickable="true" android:focusable="true" android:text="trsfnjsfksjfnjsdfjksdhfjksdjkfhdsfsdddddddddddddddddddddddddd" /> <com.example.myapplication.MyTextView android:id="@+id/myview" android:layout_width="1dp" android:layout_height="2dp" android:clickable="true" android:enabled="false" android:focusable="true" app:testAttr="520" app:text="helloWorld" /> <com.example.myapplication.RectView android:layout_width="wrap_content" android:layout_height="wrap_content" /> <com.example.myapplication.MyView1 android:layout_width="wrap_content" android:layout_height="wrap_content" /> <com.example.myapplication.DividingLineTextView android:id="@+id/divide" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" /> </LinearLayout>Copy the code

Wrap_content = wrap_conten; wrap_conten = wrap_conten; wrap_conten = wrap_conten;

More code, we combine annotation analysis.

public class MyHorizonView extends ViewGroup { private static final String TAG = "HorizontaiView"; private List<View> mMatchedChildrenList = new ArrayList<>(); public MyHorizonView(Context context) { super(context); } public MyHorizonView(Context context, AttributeSet attributes) { super(context, attributes); } public MyHorizonView(Context context, AttributeSet attributes, int defStyleAttr) { super(context, attributes, defStyleAttr); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childCount = getChildCount(); int left = 0; View child; for (int i = 0; i < childCount; i++) { child = getChildAt(i); if (child.getVisibility() ! = View.GONE) { int childWidth = child.getMeasuredWidth(); Child.layout (left, 0, left + childWidth, Child.getMeasuredHeight ()); left += childWidth; } } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); mMatchedChildrenList.clear(); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); / / if not determine the value of the specification is AT_MOST, with the parent View wide high final Boolean measureMatchParentChildren = heightSpecMode! = MeasureSpec.EXACTLY || widthSpecMode ! = MeasureSpec.EXACTLY; int childCount = getChildCount(); View child; for (int i = 0; i < childCount; i++) { child = getChildAt(i); if (child.getVisibility() ! = View.GONE) { final LayoutParams layoutParams = child.getLayoutParams(); measureChild(child, widthMeasureSpec, heightMeasureSpec); If (measureMatchParentChildren) {/ / need to calculate the height of the parent View to measure the child to View the if (layoutParams. Width = = layoutParams. MATCH_PARENT | | layoutParams.height == LayoutParams.MATCH_PARENT) { mMatchedChildrenList.add(child); }}}} if (widthSpecMode == MeasureSpec.AT_MOST &&heightSpecMode == MeasureSpec.AT_MOST) {// If (widthSpecMode == MeasureSpec. SetMeasuredDimension (getMeasuredWidth(), getMeasuredHeight())); } else if (widthSpecMode == MeasureSpec.AT_MOST) {// If (widthSpecMode == MeasureSpec.AT_MOST) {// If (widthSpecMode == MeasureSpec. SetMeasuredDimension (getMeasuredWidth(), heightSpecSize); } else if (heightSpecMode == measurespec.at_most) {// If (heightSpecMode == measurespec.at_most) { SetMeasuredDimension (widthSpecSize, getMeasuredHeight()); } for (int i = 0; i < mMatchedChildrenList.size(); i++) { View matchChild = getChildAt(i); if (matchChild.getVisibility() ! = View.GONE) { final LayoutParams layoutParams = matchChild.getLayoutParams(); // childWidthMeasureSpec final int childWidthMeasureSpec; if (layoutParams.width == LayoutParams.MATCH_PARENT) { childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY); } else { childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, layoutParams.width); } // Calculate the child View height MeasureSpec final int childHeightMeasureSpec; if (layoutParams.height == LayoutParams.MATCH_PARENT) { childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY); } else { childHeightMeasureSpec = getChildMeasureSpec(widthMeasureSpec, 0, layoutParams.height); Matchchild. measure(childWidthMeasureSpec, childHeightMeasureSpec); }}}}Copy the code

Here we just overwrite two important methods in the drawing process: onMeasure and onLayout methods.

The specific logic of onMeasure method is as follows:

  1. Super. onMeasure calculates the size of the custom view first.
  2. Call measureChild to measureChild views;
  3. Custom view width and height is not EXACTLY MeasureSpec.EXACTLY if the child is match_parent, extra processing is required for MeasureSpec.AT_MOST.
  4. When the size of the custom View is determined, the child View is match_parent.

The above measurement process code is also referred to the FrameLayout source code, see the article for details:

For the onLayout method, because it slides horizontally, the layout is based on the width.

At this point, our View layout is pretty much finished. But to achieve the Viewpager effect, you also need to add event handling. Event processing process before we have analyzed, in the production of custom View is often used, do not understand can refer to the article Android Touch event distribution super detailed analysis.

private void init(Context context) { mScroller = new Scroller(context); mTracker = VelocityTracker.obtain(); } /** * Since we are defining a ViewGroup, we start with onInterceptTouchEvent. * * @param Event * @return */ @override public Boolean onInterceptTouchEvent(MotionEvent event) { boolean intercepted = false; int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: intercepted = false; // It must not be intercepted, otherwise all subsequent ACTION_MOME and ACTION_UP events will be intercepted. break; case MotionEvent.ACTION_MOVE: intercepted = Math.abs(x - mLastX) > Math.abs(y - mLastY); break; } Log.d(TAG, "onInterceptTouchEvent: intercepted " + intercepted); mLastX = x; mLastY = y; return intercepted ? intercepted : super.onInterceptHoverEvent(event); } /** * After the ViewGroup intercepts the user's horizontal swipe event, subsequent Touch events are delivered to 'onTouchEvent' for processing. Override public Boolean onTouchEvent(MotionEvent) {mtracker.addMovement (event); int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: int deltaX = x - mLastX; Log.d(TAG, "onTouchEvent: deltaX " + deltaX); // The scrollBy method will offset the position of our current View scrollBy(-deltax, 0); break; case MotionEvent.ACTION_UP: Log.d(TAG, "onTouchEvent: " + getScrollX()); // getScrollX() is cheap in the X direction, Int Distance = getScrollX() -mchildWidth * mCurrentIndex; Math.abs(distance) > mChildWidth /2) {if (distance > 0) {mCurrentIndex++; } else { mCurrentIndex--; }} else {/ / get the X axis acceleration, units for the unit, the default for pixels, here is 1000 pixels per second mTracker.com puteCurrentVelocity (1000); float xV = mTracker.getXVelocity(); View if (math.abs (xV) >50) {if (xV < 0) {mCurrentIndex++; // if (math.abs (xV) >50) {mCurrentIndex++; } else { mCurrentIndex--; GetChildCount () -1) mCurrentIndex = mCurrentIndex < 0? 0 : mCurrentIndex > getChildCount() - 1 ? getChildCount() - 1 : mCurrentIndex; // Slide to the next View smoothScrollTo(mCurrentIndex * mChildWidth, 0); mTracker.clear(); break; } Log.d(TAG, "onTouchEvent: "); mLastX = x; mLastY = y; return super.onTouchEvent(event); } @Override public boolean dispatchTouchEvent(MotionEvent ev) { return super.dispatchTouchEvent(ev); } private void smoothScrollTo(int destX, int destY) {// startScroll method will generate a series of offsets from (getScrollX(), getScrollY()), Destx-getscrollx () and desty-getScrolly () indicate the moving distances mscroll. startScroll(getScrollX(), getScrollY(), destx-getScrollX (), destY - getScrollY(), 1000); // The invalidate method redraws the View, i.e. calls the View's onDraw method, which in turn calls computeScroll(), invalidate(); } // Override public void computeScroll() {super.computeScroll(); / / when scroller.com puteScrollOffset () = true said no end if sliding (mScroller.com puteScrollOffset ()) {/ / call scrollTo method for sliding, ScrollTo (mscroll.getCurrx (), mscroll.getcurry ())); // Continue to refresh View postInvalidate() without swiping; }}Copy the code

The specific effect is shown in the picture below:

\

The use of Scroller is summarized as follows:

  1. Call Scroller’s startScroll() method to initialize some scrolling and then force the View to draw (redraw the View by calling invalidate() or postInvalidate() of the View);
  2. ComputeScroll () is overwritten by the drawChild method, and Scroller computeScrollOffset() is used to determine whether the scroll is over.
  3. The scrollTo() method redraws the View, but it still calls invalidate() or postInvalidate() to trigger a redraw of the interface. Redrawing the View triggers computeScroll().
  4. So reciprocating into a cycle stage, can achieve smooth rolling effect;

You might say, well, why call scrollTo() when you end up calling scrollTo()? Well, you can call scrollTo(), but scrollTo() is instantaneous, so it’s not a very good user experience. So Android provides the Scroller class for smooth scrolling.

For your understanding, I drew a simple call diagram:

 

 

So that’s the end of the custom view method. Hopefully it’s useful.

References:

1, Android custom View full solution