一、構(gòu)造方法
public MyView(Context context) { //在代碼中直接創(chuàng)建對象
super(context);
}
public MyView(Context context, @Nullable AttributeSet attrs) { //默認(rèn)調(diào)用
super(context, attrs);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { //手動顯式調(diào)用
super(context, attrs, defStyleAttr);
}
public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { //手動顯式調(diào)用
super(context, attrs, defStyleAttr, defStyleRes);
}
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.BottomView);
array.recycle();
參數(shù)解析:
- context:上下文,View當(dāng)前處于的環(huán)境。
- attrs:View的屬性參數(shù),一般在布局文件中設(shè)置。
- defStyleAttr:attrs.xml文件中的attribute,屬于自定義屬性。
- defStyleRes:styles.xml文件中的style,只有當(dāng)defStyleAttr沒有起作用,才會使用到這個值。
//自定義屬性文件,attr.xml
<declare-styleable name="SlideMenu">
<attr name="rightPadding" format="dimension"/>
</declare-styleable>
一個屬性最終的取值,有一個順序,這個順序優(yōu)先級從高到低依次是:
1.直接在XML文件中定義的 ==》布局文件。
2.在XML文件中通過style這個屬性定義的 ==》在布局中使用自定義屬性樣式。
3.通過defStyleAttr定義的 ==》在View的構(gòu)造方法中使用自定義屬性樣式。
4.通過defStyleRes定義的 ==》在View的構(gòu)造方法中使用自定義樣式。
5.直接在當(dāng)然工程的theme主題下定義的 ==》AndroidManifest.xml中設(shè)置。
二、測量流程
@Override -> 使用默認(rèn)的測量方法,由父類測量
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measuredWidth =getMeasuredWidth();
int measuredHeight =getMeasuredHeight();
//測量已經(jīng)完成,可以獲取測量的參數(shù)值
}
@Override -> 覆寫默認(rèn)的測量方法,自定義測量規(guī)則
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//測量模式
int widthMode =MeasureSpec.getMode(widthMeasureSpec);
int heightMode =MeasureSpec.getMode(heightMeasureSpec);
//測量寬高
int width =MeasureSpec.getSize(widthMeasureSpec);
int height=MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width,height);
}
測量模式解析:
- MeasureSpec.AT_MOST:父容器指定最大的空間,WRAP_CONTENT屬性
- MeasureSpec.EXACTLY:父容器指定精確的大小空間,MATCH_PARENT 或者 一個精確值
- MeasureSpec.UNSPECIFIED:父容器未指定空間的大小,父容器是ScrollerView等可滑動控件
寬高解析: - getWidth()和getHeight():View的最終大小,測量方法完成后可以得到具體的值
- getMeasureWidth()和getMeasureHeight():View的測量大小,測量完成后可以得到具體的值
- 寬的計算:控件的右邊到屏幕的左邊的距離 --- 控件的左邊到屏幕左邊的距離,即 right - left
- 高的計算:控件的下邊到屏幕的上邊的距離 --- 控件的上邊到屏幕上邊的距離,即 bottom - top
- 屬性詳解:top,控件頂部到屏幕頂部的距離。bottom,控件底部到屏幕頂部的距離。left,控件左邊到屏幕左邊的距離。right,控件右邊到屏幕左邊的距離。
//當(dāng)View的尺寸發(fā)生變化時會觸發(fā),如onMeasure完成后
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//初始化一些有關(guān)尺寸的成員變量
}
//自定義ViewGroup,還要測量子View的大小
measureChild(child, widthMeasureSpec, heightMeasureSpec);
- 設(shè)置控件自適應(yīng)屏幕時的大小(在某些特殊情況,View的AT_MOST模式可以同時對應(yīng)wrap_content和match_parent兩種布局參數(shù),所以通過測量模式來判斷控件所設(shè)置的屬性wrap_content和match_parent是不準(zhǔn)確的)
if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT && getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(mWidth, mHeight);
} else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(mWidth, heightSize);
} else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(widthSize, mHeight);
}
onMeasure、Measure、measureChild、measureChildren 的區(qū)別
1、onMeasure 測量自身,自定義View時重寫,定義控件的寬高,常在自定義的View中使用
2、Measure 測量自身,方法不可重寫,內(nèi)部調(diào)用onMeasure方法,常在自定義的ViewGroup中使用
3、measureChild 測量某個子View,內(nèi)部調(diào)用Measure方法,常在自定義的ViewGroup中使用
4、measureChildren 測量所有子View,內(nèi)部調(diào)用measureChild方法,常在自定義的ViewGroup中使用
創(chuàng)建新的測量參數(shù)
在自定義View的開發(fā)中,我們重寫測量方法,方法里的傳參(widthMeasureSpec,heightMeasureSpec)都是由父類提供的,在自定義ViewGroup的開發(fā)中,我們可以根據(jù)當(dāng)前布局的測量參數(shù),為布局內(nèi)的子控件創(chuàng)建新的測量參數(shù),來控制子View在布局的顯示大小
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize,widthMode);
三、布局流程
ViewGroup中的方法,設(shè)置子View的布局參數(shù)和顯示位置
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed) {
layoutArcButton();
int count = getChildCount();
for (int i = 0; i < count - 1; i++) {
View child = getChildAt(i + 1);
child.setVisibility(View.GONE);
//為什么-2,因為主菜單也是其中一個子控件
int left = (int) (mRadius * Math.sin(Math.PI / 2 / (count - 2) * i));
int top = (int) (mRadius * Math.cos(Math.PI / 2 / (count - 2) * i));
int width = child.getMeasuredWidth();
int height = child.getMeasuredHeight();
//菜單在左下或者右下角
if (mPosition == Position.LEFT_BOTTOM || mPosition == Position.RIGHT_BOTTOM) {
top = getMeasuredHeight() - height - top;
}
if (mPosition == Position.RIGHT_BOTTOM || mPosition == Position.RIGHT_TOP) {
left = getMeasuredWidth() - width - left;
}
//布局子View
child.layout(left, top, left + width, top + height);
}
}
}
View的生命周期方法,在Activity.onCreate()中會加載布局,在布局文件完成加載后會觸發(fā)該方法,常用于操縱子View,設(shè)置子View的布局參數(shù),如果View的創(chuàng)建不通過布局文件加載,則該方法不會被觸發(fā),例如調(diào)用View的一個參數(shù)的構(gòu)造方法
@Override
protected void onFinishInflate() { -> Activity onCreate 執(zhí)行完觸發(fā)
super.onFinishInflate();
//獲取子控件個數(shù)
int count = getChildCount();
if (count == 0) return;
for (int i = 0; i < count; i++) {
View view = getChildAt(i);
LinearLayout.LayoutParams Lp = (LayoutParams) view.getLayoutParams();
Lp.weight = 0;
Lp.width = getScreenWidth() / mTabVisibleCount;
view.setLayoutParams(Lp);
}
}
View的生命周期方法,在Activity.onResume()執(zhí)行后,View被加載到窗口時觸發(fā)該方法
@Override
protected void onAttachedToWindow() { -> Activity onResume 執(zhí)行完觸發(fā)
super.onAttachedToWindow();
}
View的生命周期方法,當(dāng)View從Window中被移除時觸發(fā)該方法,常見于viewGroup.removeView(view)或者當(dāng)前View所依賴的Activity被銷毀了
@Override
protected void onDetachedFromWindow() { -> View 移出窗口時觸發(fā)
super.onDetachedFromWindow();
}
自定義View的布局類型(線性、相對、流式等)
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
layout()、onLayout()、requestLayout()的區(qū)別
1、layout:指定View新的顯示位置,用法:view.layout(left,top,right,bottom);
2、onLayout:設(shè)置View的顯示位置,用法:重寫該方法,定義View的顯示規(guī)則
3、requestLayout:強(qiáng)制View重新布局,用法:view.requestLayout();
注意:View的生命周期方法均在Activity.onResume()方法后執(zhí)行,所以在此之前,是獲取不到View的屬性的,比如在Activity.onCreate()中獲取View的寬高,盡管View已經(jīng)被創(chuàng)建,但得到View的寬高均為0,因為View的生命周期方法未得到執(zhí)行,View還未經(jīng)過測量,所有屬性均是默認(rèn)值
View 生命周期方法執(zhí)行的先后順序
onFinishInflate -> onAttachedToWindow -> onMeasure -> onSizeChanged -> onLayout -> onDraw -> onDetachedFromWindow
四、繪制流程
繪制方法
@Override
protected void onDraw(Canvas canvas) {
canvas.drawARGB(255,255,255,255);
}
刷新界面,重新繪制
private void invalidateView() {
if (Looper.getMainLooper() == Looper.myLooper()) { //UI線程
invalidate();
} else { //非UI線程
postInvalidate();
}
}
ViewGroup中的方法,用于繪制子控件
@Override
protected void dispatchDraw(Canvas canvas) {
canvas.save();
canvas.translate(mInitTranslationX + mTranslationX, getHeight() + 2);
canvas.drawPath(mPath, mPaint);
canvas.restore();
super.dispatchDraw(canvas);
}
五、坐標(biāo)系
- View自身的坐標(biāo)
- getTop():獲取View自身頂邊到其父布局頂邊的距離
- getLeft():獲取View自身左邊到其父布局左邊的距離
- getRight():獲取View自身右邊到其父布局左邊的距離
- getBottom():獲取View自身底邊到其父布局頂邊的距離
- MotionEvent提供的方法
- getX():獲取點擊事件距離控件左邊的距離,即視圖坐標(biāo)
- getY():獲取點擊事件距離控件頂邊的距離,即視圖坐標(biāo)
- getRawX():獲取點擊事件距離屏幕左邊的距離,即絕對坐標(biāo)
- getRawY():獲取點擊事件距離屏幕左邊的距離,即絕對坐標(biāo)
六、View的滑動
- layout(left,top,right,bottom); View的坐標(biāo)點
- offsetLeftAndRight(offsetX) 和 offsetTopAndBottom(offsetY); View的偏移量
- MarginLayoutParams.leftMargin ,設(shè)置View的邊距改變View的位置
- scrollTo(x,y) 和 scrollBy(x,y); 絕對位移和相對位移,瞬間完成
- Scroller , 帶有過渡動畫的滑動,擁有良好的用戶體驗
public class CustomView extends View {
private Scroller mScroller;
public CustomView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mScroller = new Scroller(context);
}
//系統(tǒng)會在繪制View的時候在draw中調(diào)用該方法
@Override
public void computeScroll() {
super.computeScroll();
if(mScroller.computeScrollOffset()){
((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
invalidate();
}
}
public void smoothScrollTo(int destX,int destY){
int scrollX =getScrollX();
int delta = destX- scrollX;
mScroller.startScroll(scrollX,0,delta,0,2000);
invalidate();
}
}
- getScrollX() 和 getScrollY() ,View的左上角的點到View的父視圖左上角的點之間的X軸方向和Y軸方向的值
七、VelocityTracker
//速度跟蹤器
private VelocityTracker mVelocityTracker;
//初始化滑動速度跟蹤類
mVelocityTracker = VelocityTracker.obtain();
//計算一秒內(nèi)的滑動速度
mVelocityTracker.computeCurrentVelocity(1000);
//獲取X方向的滑動速度
mVelocityTracker.getXVelocity();
//獲取Y方向的滑動速度
mVelocityTracker.getYVelocity();
@Override //回收資源
protected void onDetachedFromWindow() {
mVelocityTracker.recycle();
super.onDetachedFromWindow();
}
八、事件分發(fā)機(jī)制
Android的事件分發(fā)可以理解為向下分發(fā),向上回傳,類似V字型,V字的左邊是事件進(jìn)行向下分發(fā),如果途中沒有進(jìn)行事件的分發(fā)攔截,則事件傳遞到最底層的View,即是最接近屏幕的View。V字的右邊是事件的回傳,如果中途沒有進(jìn)行事件的消費,則事件傳遞到最頂層的View,直至消失。
@Override //事件分發(fā),默認(rèn)false
public boolean dispatchTouchEvent(MotionEvent ev) {
return super.dispatchTouchEvent(ev);
}
@Override //事件攔截,默認(rèn)false
public boolean onInterceptTouchEvent(MotionEvent ev) {
return super.onInterceptTouchEvent(ev);
}
@Override //事件消費,默認(rèn)false
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
- dispatchTouchEvent:Android所有的事件都經(jīng)過此方法分發(fā),返回true則表示對事件不再進(jìn)行分發(fā),事件沒有被消費,返回false則繼續(xù)往下分發(fā),如果是ViewGroup,則分發(fā)給onInterceptTouchEvent決定事件繼續(xù)分發(fā)還是被攔截
- onInterceptTouchEvent:解決滑動沖突的外部攔截法,是ViewGroup中特有的方法,決定觸摸事件是否攔截,返回true則表示攔截事件的分發(fā),交給自身的onTouchEvent進(jìn)行處理,返回false,則事件繼續(xù)向下分發(fā)
- onTouchEvent:Android的觸摸事件消費,返回true,則表示當(dāng)前View處理該事件,返回false,則事件繼續(xù)進(jìn)行分發(fā),子View處理事件的優(yōu)先級比父View處理事件的優(yōu)先級要高
- requestDisallowInterceptTouchEvent:解決滑動沖突的內(nèi)部攔截法,作用于dispatchTouchEvent,請求不允許攔截觸摸事件,有時候,子類并不希望父類攔截它的觸摸事件,想將觸摸事件交給自身處理,常用方式:view.getParent().requestDisallowInterceptTouchEvent(true);
- 當(dāng)前View接收到的觸摸事件,可以通過調(diào)用dispatchTouchEvent方法,將觸摸事件傳遞給其他View,從而達(dá)到不同的View處理相同的觸摸事件,起到聯(lián)動的效果,比如移動A的時候,想讓B也同時移動,就可以把A的觸摸事件傳遞給B,讓B也處理這個觸摸事件
- 通過事件分發(fā)機(jī)制實現(xiàn)不能左右滑動的ViewPager
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
public class NoScrollViewPager extends ViewPager {
private boolean canScroll = false; //是否可以滑動
public NoScrollViewPager(@NonNull Context context) {
this(context,null);
}
public NoScrollViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (canScroll){
return super.onInterceptTouchEvent(ev);
}else{
return false; -> 不攔截,讓事件繼續(xù)分發(fā)
}
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (canScroll){
return super.onTouchEvent(ev);
}else {
return true; -> 消費當(dāng)前事件
}
}
}
九、View的點擊事件
View的點擊事件設(shè)置只對單個View產(chǎn)生效果,比如設(shè)置ViewGroup的根布局不可點擊,ViewGroup的子View依然能接收點擊事件。而RelativeLayout等常用布局,設(shè)置根布局不可點擊,所有子View都不會處理點擊事件。在布局強(qiáng)轉(zhuǎn)成ViewGroup時,部分功能會失效,比如設(shè)置該布局不可點擊,需要遍歷所有子View并為其設(shè)置不可點擊事件
view.setEnabled(true/false);
十、獲取View的寬高
View的measure過程與Activity的生命周期不是同步執(zhí)行的,在onCreat(),onStart(),onResume()中獲取View的寬高為零,原因是測量還沒完成
View初始化完畢時觸發(fā)
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus){
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}
投遞一個任務(wù)到消息隊列的尾部
view.post(new Runnable){
@Override
public void run(){
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}
View樹的狀態(tài)發(fā)生改變或者View樹的內(nèi)部View的可見性發(fā)生改變時觸發(fā)
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
十一、獲取View在屏幕中的位置
int position[] = new int[2]; -> 0存x , 1存y
view.getLocationInWindow(position);
//view.getLocationOnScreen(position);
TextView tv = new TextView(this); -> 生成與屏幕位置一樣的View
LayoutParams params = new LayoutParams(view.getLayoutParams());
params.leftMargin = position[0];
params.topMargin = position[1];
view.setLayoutParams(params);
viewGroup.addView(view);