Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android View體系(十一)自定義ViewGroup

Android View體系(十一)自定義ViewGroup

編輯:關於Android編程

前言

此前講了很多,終於可以講到這一節了,本文的例子是一個自定義的ViewGroup,左右滑動切換不同的頁面,類似一個特別簡化的ViewPager,這篇文章會涉及到這個系列的很多文章的內容比如View的measure、layout和draw流程,view的滑動等等,所以對View體系不大了解的同學看這篇文章前可以先從頭閱讀本系列的其他文章,再來看這篇文章效果會更好些。需要注意的是我們知道要實現一個自定義的ViewGroup是很復雜的,這個看看LineraLayout等源碼我們就會知道,這裡我們只需要把主要的功能實現就好了。

1.繼承ViewGroup

要實現自定義的ViewGroup,首先要繼承ViewGroup並調用父類構造方法,實現抽象方法等。

import android.content.Context;
import android.util.AttributeSet;
import android.view.ViewGroup;
public class HorizontalView extends ViewGroup{
    public HorizontalView(Context context) {
        super(context);
    }
    public HorizontalView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public HorizontalView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

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

這裡我們定義了名字叫HorizontalView的類並繼承 ViewGroup,onLayout這個抽象方法是必須要實現的,我們暫且什麼都不做。

2.對wrap_content屬性進行處理

在Android View體系(九)自定義View這篇文章中我們同樣對wrap_content屬性進行了處理不明白的可以查看這篇文章或者直接查看Android View體系(七)從源碼解析View的measure流程來了解具體的原因,這裡就不贅述了。

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
public class HorizontalView extends ViewGroup {
    //...省略此前的構造代碼
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        measureChildren(widthMeasureSpec, heightMeasureSpec);
        //如果沒有子元素,就設置寬高都為0(簡化處理)
        if (getChildCount() == 0) { 
            setMeasuredDimension(0, 0);
        }
        //寬和高都是AT_MOST,則設置寬度所有子元素的寬度的和;高度設置為第一個元素的高度;
        else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
            View childOne = getChildAt(0);
            int childWidth = childOne.getMeasuredWidth();
            int childHeight = childOne.getMeasuredHeight();
            setMeasuredDimension(childWidth * getChildCount(), childHeight);
        }
        //如果寬度是wrap_content,則寬度為所有子元素的寬度的和
        else if (widthMode == MeasureSpec.AT_MOST) {
            int childWidth = getChildAt(0).getMeasuredWidth();
            setMeasuredDimension(childWidth * getChildCount(), heightSize);
        }
        //如果高度是wrap_content,則高度為第一個子元素的高度
        else if (heightMode == MeasureSpec.AT_MOST) {
            int childHeight = getChildAt(0).getMeasuredHeight();
            setMeasuredDimension(widthSize, childHeight);
        }

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

這裡如果沒有子元素時采用了簡化的寫法直接將寬和高直接設置為0,正常的話我們應該根據LayoutParams中的寬和高來做相應的處理,另外我們在測量時沒有考慮它的padding和子元素的margin。

3.實現onLayout

接下來我們實現onLayout,來布局子元素,因為每一種布局方式子View的布局都是不同的,所以這個是ViewGroup唯一一個抽象方法,需要我們自己去實現:

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;

public class HorizontalView extends ViewGroup {
    //... 省略構造方法代碼和onMeasure的代碼

    @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 width = child.getMeasuredWidth();
                childWidth = width; 
                child.layout(left, 0, left + width, child.getMeasuredHeight());
                left += width;
            }
        }
    }

遍歷所有的子元素,如果子元素不是GONE,則調用子元素的layout方法將其放置到合適的位置上,相當於默認第一個子元素占滿了屏幕,後面的子元素就是在第一個屏幕後面緊挨著和屏幕一樣大小的後續元素,所以left是一直累加的,top保持0,bottom保持第一個元素的高度,right就是left+元素的寬度,同樣這裡沒有處理自身的pading以及子元素的margin。

4.處理滑動沖突

這個自定義ViewGroup是水平滑動,如果裡面是ListView,則ListView是垂直滑動,如果我們檢測到的滑動方向是水平的話,就讓父View攔截用來進行View的滑動切換 :

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;

public class HorizontalView extends ViewGroup {
    private int lastInterceptX;
    private int lastInterceptY;
    private int lastX;
    private int lastY;
    //... 省略了構造函數的代碼

    @Override
    public boolean onInterceptHoverEvent(MotionEvent event) {
        boolean intercept = false;
        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 - lastInterceptX; 
                int deltaY = y - lastInterceptY; 
                //用戶想水平滑動的,所以攔截
                if (Math.abs(deltaX) - Math.abs(deltaY) > 0) { 
                    intercept = true; 
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        lastX = x;
        lastY = y;
        lastInterceptX = x; 
        lastInterceptY = y;
        return intercept;
    }
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }
    //... 省略了onMeasure和onLayout的代碼
}

5.彈性滑動到其他頁面

這裡就會進入onTouchEvent事件,然後我們需要進行滑動切換頁面,這裡需要用到Scroller,具體請查看Android View體系(二)實現View滑動的六種方法這篇文章,而Scroller滑動的原理請查看 Android View體系(四)從源碼解析Scroller這篇文章。

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;

public class HorizontalView extends ViewGroup {
    //... 省略構造函數,init方法,onInterceptTouchEvent
    int lastInterceptX;
    int lastInterceptY;
    int lastX;
    int lastY;
    int currentIndex = 0; //當前子元素
    int childWidth = 0; 
    private Scroller scroller;
    @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: 
             //相對於當前View滑動的距離,正為向左,負為向右
                int distance = getScrollX() - currentIndex * childWidth;
                //滑動的距離要大於1/2個寬度,否則不會切換到其他頁面
                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);
    }
    //...省略onMeasure方法
     @Override
    public void computeScroll() {
        super.computeScroll();
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
            postInvalidate();
        }
    }
    //彈性滑動到指定位置
    public void smoothScrollTo(int destX, int destY) {
        scroller.startScroll(getScrollX(), getScrollY(), destX - getScrollX(), destY - getScrollY(), 1000); 
        invalidate();
    }
    @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);
            int width = child.getMeasuredWidth();
            //賦值為子元素的寬度
            childWidth = width; 
            child.layout(left, 0, left + width, child.getMeasuredHeight());
            left += width;
        }
    }
}

6.快速滑動到其他頁面

我們不只滑動超過一半才切換到上/下一個頁面,如果滑動速度很快的話,我們也可以判定為用戶想要滑動到其他頁面,這樣的體驗也是好的。 這部分也是在onTouchEvent中的ACTION_UP部分:
這裡又需要用到VelocityTracker,它用來測試滑動速度的。使用方法也很簡單,首先在構造函數中進行初始化,也就是前面的init方法中增加一條語句:

 ...
  private VelocityTracker tracker;    
  ...
  public void init() {
        scroller = new Scroller(getContext());
        tracker=VelocityTracker.obtain();
    }
  ...

接著改寫onTouchEvent部分:


@Override
    public boolean onTouchEvent(MotionEvent event) {
...
  case MotionEvent.ACTION_UP:
              //相對於當前View滑動的距離,正為向左,負為向右
                int distance = getScrollX() - currentIndex * childWidth; 
                //必須滑動的距離要大於1/2個寬度,否則不會切換到其他頁面
                if (Math.abs(distance) > childWidth / 2) {
                    if (distance > 0) {
                        currentIndex++;
                    } else {
                        currentIndex--;
                    }
                }
                else {
                //調用該方法計算1000ms內滑動的平均速度   
                 tracker.computeCurrentVelocity(1000);
                    float xV = tracker.getXVelocity(); //獲取到水平方向上的速度
                    //如果速度的絕對值大於50的話,就認為是快速滑動,就執行切換頁面
                    if (Math.abs(xV) > 50) { 
                    //大於0切換上一個頁面
                        if (xV > 0) { 
                            currentIndex--;
                    //小於0切換到下一個頁面
                        } else { 
                            currentIndex++;
                        }
                    }
                }
                currentIndex = currentIndex < 0 ? 0 : currentIndex > getChildCount() - 1 ? getChildCount() - 1 : currentIndex;
                smoothScrollTo(currentIndex * childWidth, 0);
                //重置速度計算器
                tracker.clear();
                break;
             }

7.再次觸摸屏幕阻止頁面繼續滑動

當我們快速向左滑動切換到下一個頁面的情況,在手指釋放以後,頁面會彈性滑動到下一個頁面,可能需要一秒才完成滑動,這個時間內,我們再次觸摸屏幕,希望能攔截這次滑動,然後再次去操作頁面。
要實現在彈性滑動過程中再次觸摸攔截,肯定要在onInterceptTouchEvent中的ACTION_DOWN中去判斷,如果在ACTION_DOWN的時候,scroller還沒有完成,說明上一次的滑動還正在進行中,則直接終端scroller:

...
 @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {

        boolean intercept = false;
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: 
                intercept = false;

               //如果動畫還沒有執行完成,則打斷
                if (!scroller.isFinished()) {
                    scroller.abortAnimation();
                }
                break;
            case MotionEvent.ACTION_MOVE:          
                int deltaX = x - lastInterceptX;
                int deltaY = y - lastInterceptY;
                if (Math.abs(deltaX) - Math.abs(deltaY) > 0) { 
                    intercept = true;
                } else {
                    intercept = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercept = false;
                break;
        }
        //因為DOWN返回true,所以onTouchEvent中無法獲取DOWN事件,所以這裡要負責設置lastX,lastY
        lastX = x;
        lastY = y;
        lastInterceptX = x;
        lastInterceptY = y;
        return intercept;
    }
...

8.應用HorizontalView

首先我們在主布局中引用HorizontalView,它作為父容器,裡面有兩個ListView:



    
       
        
    

接著在代碼中為ListView填加數據:

package com.example.liuwangshu.mooncustomviewgroup;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;

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);
        lv_one=(ListView)this.findViewById(R.id.lv_one);
        lv_two=(ListView)this.findViewById(R.id.lv_two);
        String[] strs1 = {"1","2","3","4","5","6","7","8","9","10","11","12","13","14","15"};
        ArrayAdapter adapter1 = new ArrayAdapter(this,android.R.layout.simple_expandable_list_item_1,strs1);
        lv_one.setAdapter(adapter1);

        String[] strs2 = {"A","B","C","D","E","F","G","H","I","J","K","L","M","N","O"};
        ArrayAdapter adapter2 = new ArrayAdapter(this,android.R.layout.simple_expandable_list_item_1,strs2);
        lv_two.setAdapter(adapter2);
    }
}

運行程序查看效果(錄制有些問題listview的分割線顯示不出來):

這裡寫圖片描述

最後貼上HZ喎?/kf/ware/vc/" target="_blank" class="keylink">vcml6b250YWxWaWV3tcTUtMLro7o8L3A+DQo8cHJlIGNsYXNzPQ=="brush:java;"> package com.example.liuwangshu.mooncustomviewgroup; import android.content.Context; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.VelocityTracker; import android.view.View; import android.view.ViewGroup; import android.widget.Scroller; public class HorizontalView extends ViewGroup { private int lastX; private int lastY; private int currentIndex = 0; //當前子元素 private int childWidth = 0; private Scroller scroller; private VelocityTracker tracker; //增加速度檢測,如果速度比較快的話,就算沒有滑動超過一半的屏幕也可以 private int lastInterceptX=0; private int lastInterceptY=0; public HorizontalView(Context context) { super(context); init(); } public HorizontalView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public HorizontalView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } public void init() { scroller = new Scroller(getContext()); tracker = VelocityTracker.obtain(); } //todo intercept的攔截邏輯 @Override public boolean onInterceptTouchEvent(MotionEvent event) { boolean intercept = false; int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: intercept = false; //如果動畫還沒有執行完成,則打斷 if (!scroller.isFinished()) { scroller.abortAnimation(); } break; case MotionEvent.ACTION_MOVE: int deltaX = x - lastInterceptX; int deltaY = y - lastInterceptY; //水平方向距離長 MOVE中返回true一次,後續的MOVE和UP都不會收到此請求 if (Math.abs(deltaX) - Math.abs(deltaY) > 0) { intercept = true; Log.i("wangshu","intercept = true"); } else { intercept = false; Log.i("wangshu","intercept = false"); } break; case MotionEvent.ACTION_UP: intercept = false; break; } //因為DOWN返回true,所以onTouchEvent中無法獲取DOWN事件,所以這裡要負責設置lastX,lastY lastX = x; lastY = y; lastInterceptX = x; lastInterceptY = y; return intercept; } @Override public boolean onTouchEvent(MotionEvent event) { tracker.addMovement(event); int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: if (!scroller.isFinished()) { scroller.abortAnimation(); } break; case MotionEvent.ACTION_MOVE: //跟隨手指滑動 int deltaX = x - lastX; scrollBy(-deltaX, 0); break; //釋放手指以後開始自動滑動到目標位置 case MotionEvent.ACTION_UP: //相對於當前View滑動的距離,正為向左,負為向右 int distance = getScrollX() - currentIndex * childWidth; //必須滑動的距離要大於1/2個寬度,否則不會切換到其他頁面 if (Math.abs(distance) > childWidth / 2) { if (distance > 0) { currentIndex++; } else { currentIndex--; } } else { tracker.computeCurrentVelocity(1000); float xV = tracker.getXVelocity(); if (Math.abs(xV) > 50) { if (xV > 0) { currentIndex--; } else { currentIndex++; } } } currentIndex = currentIndex < 0 ? 0 : currentIndex > getChildCount() - 1 ? getChildCount() - 1 : currentIndex; smoothScrollTo(currentIndex * childWidth, 0); tracker.clear(); break; default: break; } lastX = x; lastY = y; return true; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); //測量所有子元素 measureChildren(widthMeasureSpec, heightMeasureSpec); //處理wrap_content的情況 if (getChildCount() == 0) { setMeasuredDimension(0, 0); } else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) { View childOne = getChildAt(0); int childWidth = childOne.getMeasuredWidth(); int childHeight = childOne.getMeasuredHeight(); setMeasuredDimension(childWidth * getChildCount(), childHeight); } else if (widthMode == MeasureSpec.AT_MOST) { View childOne = getChildAt(0); int childWidth = childOne.getMeasuredWidth(); setMeasuredDimension(childWidth * getChildCount(), heightSize); } else if (heightMode == MeasureSpec.AT_MOST) { int childHeight = getChildAt(0).getMeasuredHeight(); setMeasuredDimension(widthSize, childHeight); } } @Override public void computeScroll() { super.computeScroll(); if (scroller.computeScrollOffset()) { scrollTo(scroller.getCurrX(), scroller.getCurrY()); postInvalidate(); } } public void smoothScrollTo(int destX, int destY) { scroller.startScroll(getScrollX(), getScrollY(), destX - getScrollX(), destY - getScrollY(), 1000); invalidate(); } @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 width = child.getMeasuredWidth(); childWidth = width; //賦值給子元素寬度變量 child.layout(left, 0, left + width, child.getMeasuredHeight()); left += width; } } } }

github源碼下載

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved