Custom viewgroups can be divided into three categories according to their parent classes: inherited from ViewGroup, inherited from system specific ViewGroup (such as LinearLayout) and inherited from View.
The second is the simplest and the third is the most complex. Let's focus on the first case with moderate difficulty.
target
Follow ViewPager to complete a horizontal page turning view, and support sliding left and right to switch different pages.
start
Inherit ViewGroup
First, we create a HorizontalView class and implement its abstract methods.
public class HorinzontalView extends ViewGroup { public HorinzontalView(Context context) { super(context); } public HorinzontalView(Context context, AttributeSet attrs) { super(context, attrs); } public HorinzontalView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } public HorinzontalView(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) { } }
Process wrap_content
Those who have read my last blog must know that custom controls must be wrapped first_ Content. Here we need to override the onMeasure method.
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //For wrap_content int widthMode = MeasureSpec.getMode(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); measureChildren(widthMeasureSpec, heightMeasureSpec); //If there is no child View, set the width and height to 0 if (getChildCount() == 0){ setMeasuredDimension(0,0); }else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST){ //Both width and height are AT_MOST, the width is set to the sum of the widths of all child elements, and the height is set to the height of the first child element View childOne = getChildAt(0); int childWidth = childOne.getMeasuredWidth(); int childHeight = childOne.getMeasuredHeight(); setMeasuredDimension(childWidth * getChildCount(), childHeight); }else if (widthMode == MeasureSpec.AT_MOST){ //Width AT_MOST, the width is set to the sum of the widths of all child elements int childWidth = getChildAt(0).getMeasuredWidth(); setMeasuredDimension(childWidth * getChildCount(), heightSize); }else if (heightMode == MeasureSpec.AT_MOST){ //Gao is AT_MOST, the height is set to the height of the first child element int childHeight = getChildAt(0).getMeasuredHeight(); setMeasuredDimension(widthSize, childHeight); } }
Implement onLayout
Of course, after the onMeasure method measures, you also need onLayout to lay out the control.
@Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childCount = getChildCount(); int left = 0; View child; //Traversal subview for (int i = 0; i < childCount; i++) { child = getChildAt(i); if (child.getVisibility() != View.GONE){ //If the View is not GONE, place it in the appropriate location int width = child.getMeasuredWidth(); //The four parameters are: //l – Left position, relative to parent //t – Top position, relative to parent //r – Right position, relative to parent //b – Bottom position, relative to parent child.layout(left, 0, left+width, 0 + child.getMeasuredHeight()); left += width; } } }
Handling sliding conflicts
Our control slides horizontally. If its content is a vertical sliding ListView, if we do not deal with it, it will lead to sliding conflict (because the click event is captured by the outer horizon view and cannot be conveyed to the content ListView).
The idea to solve the sliding conflict is: if we detect that the sliding direction is horizontal, let the parent View intercept, otherwise, do not intercept.
class HorinzontalView extends ViewGroup { //Used to handle sliding conflicts private int lastInterceptX; private int lastInterceptY; private int lastX; private int lastY; ... @Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercept = false; int x = (int) ev.getX(); int y = (int) ev.getY(); switch (ev.getAction()){ case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: int deltaX = x - lastInterceptX; int deltaY = y - lastInterceptY; if (Math.abs(deltaX) - Math.abs(deltaY) > 0){ //Sliding is horizontal, intercepting intercept = true; } break; case MotionEvent.ACTION_UP: break; } lastX = x; lastY = y; lastInterceptX = x; lastInterceptY = y; return intercept; } @Override public boolean onTouchEvent(MotionEvent event) { //The slip time of interception will be processed here return super.onTouchEvent(event); } }
Elastic sliding effect
We need to use Scroller to slide the page
... int currentIndex = 0; int childWidth = 0; private Scroller scroller; ... @Override public boolean onTouchEvent(MotionEvent event) { //Handle intercepted click events here 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 - lastX; scrollBy(-deltaX, 0); break; case MotionEvent.ACTION_UP: int distance = getScrollX() - currentIndex * childWidth; //If the sliding distance is more than half of childWidth //Then slide to the upper / lower sub View if (Math.abs(distance) > childWidth/2){ if (distance > 0){ currentIndex++; }else{ currentIndex--; } } smoothScrollTo(currentIndex * childWidth, 0); break; } lastX = x; lastY = y; return super.onTouchEvent(event); } @Override public void computeScroll() { super.computeScroll(); if(scroller.computeScrollOffset()){ scrollTo(scroller.getCurrX(), scroller.getCurrY()); postInvalidate(); } } private void smoothScrollTo(int destX, int destY) { scroller.startScroll(getScrollX(), getScrollY(), destX - getScrollX(), destY - getScrollY(), 1000); invalidate(); }
Swipe quickly to other pages
In many cases, users will not slide for a long distance, but will slide relatively short and fast. We also need to adapt the fast sliding. In order to capture the sliding speed, we need to borrow the speed tracker VelocityTracker.
First, we need to add the code to initialize the speed tracker in the constructor.
private void init(){ scroller = new Scroller(getContext()); //Think about it, why call the obtain method here instead of new an object? tracker = VelocityTracker.obtain(); }
Then we add the code related to the sliding speed to the logic for handling click events:
@Override public boolean onTouchEvent(MotionEvent 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 - lastX; scrollBy(-deltaX, 0); break; case MotionEvent.ACTION_UP: int distance = getScrollX() - currentIndex * childWidth; if (Math.abs(distance) > childWidth/2){ if (distance > 0){ currentIndex++; }else{ currentIndex--; } }else{ //Calculate the current sliding speed tracker.computeCurrentVelocity(10); float xV = tracker.getXVelocity(); //If the sliding speed is greater than 50, "fast sliding" is considered to have occurred if (Math.abs(xV) > 50){ if (xV > 0){ currentIndex--; }else{ currentIndex++; } } } currentIndex = currentIndex < 0 ? 0 : Math.min(currentIndex, getChildCount() - 1); smoothScrollTo(currentIndex * childWidth, 0); //Reset speed calculator tracker.clear(); break; default: break; } lastX = x; lastY = y; return super.onTouchEvent(event); }
When sliding, click the screen to prevent sliding
When we slide to the next page, because elastic sliding takes time, within this time, we click the screen again, hoping to intercept this sliding, and then operate the page.
To implement the above logic, we need to make a judgment in the onInterceptEvent method, if in action_ If the Scroller has not finished executing when down, it means that the last sliding is still in progress. At this time, we can interrupt the sliding.
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercept = false; int x = (int) ev.getX(); int y = (int) ev.getY(); switch (ev.getAction()){ case MotionEvent.ACTION_DOWN: intercept = false; if (!scroller.isFinished()){ //If the Scroller does not complete its execution, break it scroller.abortAnimation(); } break; case MotionEvent.ACTION_MOVE: int deltaX = x - lastInterceptX; int deltaY = y - lastInterceptY; //Sliding is transverse intercept = Math.abs(deltaX) - Math.abs(deltaY) > 0; break; case MotionEvent.ACTION_UP: break; default: break; } lastX = x; lastY = y; lastInterceptX = x; lastInterceptY = y; return intercept; }
Apply HorizontalView
Now, our control has begun to take shape. Let's simply use it~
public class MainActivity extends AppCompatActivity { private ListView lv_one; private ListView lv_two; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); init(); } private void init() { lv_one = findViewById(R.id.lv_one); lv_two = findViewById(R.id.lv_two); List<String> strs1 = new ArrayList<>(); List<Character> strs2 = new ArrayList<>(); for (int i = 0; i < 15; i++) { strs1.add(String.valueOf(i+1)); strs2.add((char) ('A' + i)); } ArrayAdapter<String> arrayAdapter1 = new ArrayAdapter<>(this, android.R.layout.simple_expandable_list_item_1, strs1); ArrayAdapter<Character> arrayAdapter2 = new ArrayAdapter<>(this, android.R.layout.simple_expandable_list_item_1, strs2); lv_one.setAdapter(arrayAdapter1); lv_two.setAdapter(arrayAdapter2); } }
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:content=".MainActivity"> <com.example.myview.HorinzontalView android:layout_width="match_parent" android:layout_height="match_parent"> <ListView android:id="@+id/lv_one" android:layout_width="match_parent" android:layout_height="match_parent"/> <ListView android:id="@+id/lv_two" android:layout_width="match_parent" android:layout_height="match_parent"/> </com.example.myview.HorinzontalView> </RelativeLayout>
After using the logic, run the program, and you will get a simple ViewPager~
Further
Now you have basically completed its main functions. If you want to go further, you can start from the following aspects:
- Adapt your own padding to the Margin of the child View
- What actions are required when the page where the control is located is destroyed?
- What should I do if there is a click event in the content?
Reference article:
Android advanced light