編輯:關於Android編程
此前講了很多,終於可以講到這一節了,本文的例子是一個自定義的ViewGroup,左右滑動切換不同的頁面,類似一個特別簡化的ViewPager,這篇文章會涉及到這個系列的很多文章的內容比如View的measure、layout和draw流程,view的滑動等等,所以對View體系不大了解的同學看這篇文章前可以先從頭閱讀本系列的其他文章,再來看這篇文章效果會更好些。需要注意的是我們知道要實現一個自定義的ViewGroup是很復雜的,這個看看LineraLayout等源碼我們就會知道,這裡我們只需要把主要的功能實現就好了。
要實現自定義的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這個抽象方法是必須要實現的,我們暫且什麼都不做。
在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。
接下來我們實現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。
這個自定義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的代碼
}
這裡就會進入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;
}
}
}
我們不只滑動超過一半才切換到上/下一個頁面,如果滑動速度很快的話,我們也可以判定為用戶想要滑動到其他頁面,這樣的體驗也是好的。 這部分也是在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;
}
當我們快速向左滑動切換到下一個頁面的情況,在手指釋放以後,頁面會彈性滑動到下一個頁面,可能需要一秒才完成滑動,這個時間內,我們再次觸摸屏幕,希望能攔截這次滑動,然後再次去操作頁面。
要實現在彈性滑動過程中再次觸摸攔截,肯定要在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;
}
...
首先我們在主布局中引用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源碼下載
項目地址:https://github.com/wlkdb/GA_network_info點擊打開鏈接1、整個app分為android客戶端、java服務端和數據層,客戶
盡管網絡上已經有很多關於這個話題的優秀文章了,但還是寫了這篇文章,主要還是為了加強自己的記憶吧,自己過一遍總比看別人的分析要深刻得多,那就走起吧。簡單示例 先看一
和Android UI layout一樣,我們也可以在XML中定義應用程序的菜單。通過在菜單的onCreateOptionsMenu方法中膨脹菜單layout。這樣做會使
前言最近Android studio(下文簡稱AS)官方發布了正式版,目前火得不行。個人認為主要是因為android是google自家的產品,AS也是他自己搞的IDE,以