《Android羣英傳》讀書筆記 (2) 第三章 控件架構與自定義控件詳解 + 第四章 ListView使用技巧 + 第五章 Scroll分析

第三章 Android控件架構與自定義控件詳解

1.Android控件架構
下圖是UI界面架構圖,每一個Activity都有一個Window對象,一般是由PhoneWindow類來實現的。
PhoneWindow將DecorView做爲整個應用窗口的根View,DecorView將屏幕分紅兩部分:TitleView和ContentView。
ContentView其實是一個FrameLayout,裏面容納的就是咱們在xml佈局文件中定義的佈局。
imglinux

爲何調用requestWindowFeature()方法必定要在setContentView()方法調用以前?
當程序在onCreate()方法中調用setContentView()方法後,ActivityManagerService會回調onResume()方法,此時系統纔會將整個DecorView添加到PhoneWindow中,並讓其顯示出來,從而完成界面的繪製。android

2.View的測量:MeasureSpec和測量模式
MeasureSpec是一個32位的int值,其中高2位位測量的模式,低30位位測量的大小 (使用位運算是爲了提升效率)
測量模式有三種:
(1)EXACTLY:精確值模式,屬性設置爲精確數值或者match_parent時,系統使用的是EXACTLY模式
(2)AT_MOST:最大值模式,屬性設置爲wrap_content時,系統使用的是AT_MOST模式
(3)UNSPECIFIED:不指定大小測量模式,一般狀況下在繪製自定義View時纔會用到git

View類默認的onMeasure()方法只支持EXACTLY模式,因此若是在自定義View的時候不重寫onMeasure方法的話,就只能使用EXACTLY模式。自定義View能夠響應你指定的具體的寬高值或者是match_parent屬性,可是,若是要讓自定義View支持wrap_content屬性的話,那麼就必需要重寫onMeasure方法來指定wrap_content時view的大小。github

重寫onMeasure方法的最終工做就是把測量後的寬高值做爲參數設置給setMeasuredDimension方法。canvas

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//計算width和height
setMeasuredDimension(width, height);
}

 

3.View和ViewGroup的繪製
View的onDraw()方法包含一個參數Canvas對象,使用這個Canvas對象就能夠進行繪圖了。
一般狀況下,Canvas對象的建立須要傳入參數Bitmap,爲何呢?
這是由於傳進去的Bitmap與經過這個Bitmap建立的Canvas畫布是牢牢聯繫在一塊兒的,這個Bitmap用來存儲全部繪製在Canvas上的像素信息,當使用Bitmap建立Canvas以後,後面調用全部的Canvas.drawXXX方法都發生在這個Bitmap上。架構

ViewGroup一般不須要繪製,由於它自己沒有須要繪製的東西,若是不指定ViewGroup的背景顏色,那麼ViewGroup的onDraw方法都不會被調用。可是,ViewGroup會調用dispatchDraw方法來繪製其子view,其過程一樣是經過遍歷全部子view並調用子view的繪製方法來完成繪製工做的。ide

4.自定義View(ViewGroup)
三種自定義View的方式:
(1)對現有控件進行擴展
對現有控件進行擴展的代碼結構一般以下:佈局

@Override
protected void onDraw(Canvas canvas) {
//在回調父類方法以前實現本身的邏輯,對TextView來講就是在繪製文本以前
super.onDraw(canvas);
//在回調父類方法以後實現本身的邏輯,對TextView來講就是在繪製文本以後
}

 

例如,書中對TextView進行擴展代碼節選post

private void initView() {
mPaint1 = new Paint();
mPaint1.setColor(getResources().getColor(android.R.color.holo_blue_light));
mPaint1.setStyle(Paint.Style.FILL);
mPaint2 = new Paint();
mPaint2.setColor(Color.YELLOW);
mPaint2.setStyle(Paint.Style.FILL);
}

@Override
protected void onDraw(Canvas canvas) {
// 繪製外層矩形
canvas.drawRect(
0,
0,
getMeasuredWidth(),
getMeasuredHeight(),
mPaint1);
// 繪製內層矩形
canvas.drawRect(
10,
10,
getMeasuredWidth() - 10,
getMeasuredHeight() - 10,
mPaint2);
canvas.save();
// 繪製文字前平移10像素
canvas.translate(10, 0);
// 父類完成的方法,即繪製文本
super.onDraw(canvas);
canvas.restore();
}

 

(2)經過組合來實現新的控件
這種方式一般須要繼承一個合適的ViewGroup,再給它添加指定功能的控件,從而組合成新的複合控件。
[之前項目中使用這種方式建立應用內統一的信息界面,能夠是提示正在加載。也能夠是提示數據出錯了等]
例如,書中的TopBar例子:字體

public class TopBar extends RelativeLayout {

// 包含topbar上的元素:左按鈕、右按鈕、標題
private Button mLeftButton, mRightButton;
private TextView mTitleView;

// 佈局屬性,用來控制組件元素在ViewGroup中的位置
private LayoutParams mLeftParams, mTitlepParams, mRightParams;

// 左按鈕的屬性值,即咱們在atts.xml文件中定義的屬性
private int mLeftTextColor;
private Drawable mLeftBackground;
private String mLeftText;
// 右按鈕的屬性值,即咱們在atts.xml文件中定義的屬性
private int mRightTextColor;
private Drawable mRightBackground;
private String mRightText;
// 標題的屬性值,即咱們在atts.xml文件中定義的屬性
private float mTitleTextSize;
private int mTitleTextColor;
private String mTitle;

// 映射傳入的接口對象
private topbarClickListener mListener;
......
}

 

(3)重寫View來實現全新的控件
建立自定義View的難點在於繪製控件和實現交互,一般須要繼承View類,並重寫onDraw、onMeasure等方法來實現繪製邏輯,同時經過重寫onTouchEvent等觸控事件方法來實現交互邏輯。
[這類自定義View是比較經常使用的,本身之前也寫過幾個簡單的例子,參見AnnotationViewProgressView項目,或者參考以前的博文Android Text View with Custom Font,一個能夠自定義字體的TextView]
例如,書中的弧線展現圖例子

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
mMeasureWidth = MeasureSpec.getSize(widthMeasureSpec);
mMeasureHeigth = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(mMeasureWidth, mMeasureHeigth);
initView();
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 繪製圓
canvas.drawCircle(mCircleXY, mCircleXY, mRadius, mCirclePaint);
// 繪製弧線
canvas.drawArc(mArcRectF, 270, mSweepAngle, false, mArcPaint);
// 繪製文字
canvas.drawText(mShowText, 0, mShowText.length(), mCircleXY, mCircleXY + (mShowTextSize / 4), mTextPaint);
}

 

5.事件攔截機制分析 [TODO:此內容特別重要,後期製做特別內容詳細分析]

 

第四章 ListView使用技巧

1.使用ViewHolder模式提升效率
這種方式是必需要用的!下面的例子是一個常見的使用ViewHolder而且包含多個item type的Adapter例子:

public class ChatItemListViewAdapter extends BaseAdapter {

private List<ChatItemListViewBean> mData;
private LayoutInflater mInflater;

public ChatItemListViewAdapter(Context context, List<ChatItemListViewBean> data) {
this.mData = data;
mInflater = LayoutInflater.from(context);
}

@Override
public int getCount() {
return mData.size();
}

@Override
public Object getItem(int position) {
return mData.get(position);
}

@Override
public long getItemId(int position) {
return position;
}

@Override
public int getItemViewType(int position) {
ChatItemListViewBean bean = mData.get(position);
return bean.getType();
}

@Override
public int getViewTypeCount() {
return 2;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
if (convertView == null) {
if (getItemViewType(position) == 0) {
holder = new ViewHolder();
convertView = mInflater.inflate(R.layout.chat_item_itemin, null);
holder.icon = (ImageView) convertView.findViewById(R.id.icon_in);
holder.text = (TextView) convertView.findViewById(R.id.text_in);
} else {
holder = new ViewHolder();
convertView = mInflater.inflate(R.layout.chat_item_itemout, null);
holder.icon = (ImageView) convertView.findViewById(R.id.icon_out);
holder.text = (TextView) convertView.findViewById(R.id.text_out);
}
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.icon.setImageBitmap(mData.get(position).getIcon());
holder.text.setText(mData.get(position).getText());
return convertView;
}

public final class ViewHolder {
public ImageView icon;
public TextView text;
}

}

 

2.listview的一些屬性設置
(1)設置分隔線
android:divider=""@android:color/white"
android:dividerHeight="10dp"
android:divider="@null" (設置分隔線透明)
(2)隱藏滾動條
android:scrollbars="none"
(3)取消item的點擊效果
android:listSelector="@android:color/transparent"

3.listview的一些方法設置
(1)設置listview顯示在第幾項
listview.setSelection(n); 這個方法相似scrollTo瞬間完成移動,平滑移動可使用下面的方式
listview.smoothScrollBy(distance, duration);
listview.smoothScrollByOffset(offset);
listview.smoothScrollToPosition(index);
(2)處理空listview
listview.setEmptyView(View)

4.動態修改listview
在使用adapter的notifyDataSetChanged方法時,必須保證傳進adapter的數據list和發生數據變化的list是同一個對象,不然將沒法看到效果。

5.listview滑動監聽
監聽listview的滑動事件的方法有兩種:一個是OnTouchListener來實現監聽,另外一個是使用OnScrollListener來實現監聽。
例如,書中實現了一個監聽listview上下滑動事件操縱toolbar顯示和隱藏效果的例子:

public class ScrollHideListView extends Activity {

private Toolbar mToolbar;
private ListView mListView;
private String[] mStr = new String[20];
private int mTouchSlop;
private float mFirstY;
private float mCurrentY;
private int direction;
private ObjectAnimator mAnimator;
private boolean mShow = true;

View.OnTouchListener myTouchListener = new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mFirstY = event.getY();
break;
case MotionEvent.ACTION_MOVE:
mCurrentY = event.getY();
if (mCurrentY - mFirstY > mTouchSlop) {
direction = 0;// down
} else if (mFirstY - mCurrentY > mTouchSlop) {
direction = 1;// up
}
if (direction == 1) {
if (mShow) {
toolbarAnim(1);//show
mShow = !mShow;
}
} else if (direction == 0) {
if (!mShow) {
toolbarAnim(0);//hide
mShow = !mShow;
}
}
break;
case MotionEvent.ACTION_UP:
break;
}
return false;
}
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.scroll_hide);
mTouchSlop = ViewConfiguration.get(this).getScaledTouchSlop();
mToolbar = (Toolbar) findViewById(R.id.toolbar);
mListView = (ListView) findViewById(R.id.listview);
for (int i = 0; i < mStr.length; i++) {
mStr[i] = "Item " + i;
}
View header = new View(this);
header.setLayoutParams(new AbsListView.LayoutParams(
AbsListView.LayoutParams.MATCH_PARENT,
(int) getResources().getDimension( R.dimen.abc_action_bar_default_height_material)));
mListView.addHeaderView(header);
mListView.setAdapter(new ArrayAdapter<String>(
ScrollHideListView.this,
android.R.layout.simple_expandable_list_item_1,
mStr));
mListView.setOnTouchListener(myTouchListener);
}

private void toolbarAnim(int flag) {
if (mAnimator != null && mAnimator.isRunning()) {
mAnimator.cancel();
}
if (flag == 0) {
mAnimator = ObjectAnimator.ofFloat(mToolbar, "translationY", mToolbar.getTranslationY(), 0);
} else {
mAnimator = ObjectAnimator.ofFloat(mToolbar, "translationY", mToolbar.getTranslationY(), -mToolbar.getHeight());
}
mAnimator.start();
}
}

 

監聽listview的OnScrollListener的通常實現

mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
switch (scrollState){
case SCROLL_STATE_IDLE://滑動中止時
break;
case SCROLL_STATE_TOUCH_SCROLL://正在滑動時
break;
case SCROLL_STATE_FLING://手指拋動以後listview因爲慣性繼續滑動
break;
}
}

@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
//firstVisibleItem 第一個可見的item的id
//visibleItemCount 可見的item的總數
//totalItemCount 全部item的總數
}
});

 

得到當前可視的item的位置等信息的便捷方法

mListView.getLastVisiblePosition();//獲取可視區域最後一個item的id
mListView.getFirstVisiblePosition();//獲取可視區域第一個item的id

 

 

第五章 Android Scroll分析

1.獲取座標值的各類方法
圖片來自Android中的座標系以及獲取座標的方法
img

2.實現滑動的基本思想
當觸摸view時,系統記下當前觸摸點座標;當手指移動時,系統記下移動後的觸摸點座標,從而獲取到相對於前一次座標點的偏移量,並經過偏移量來修改view的座標,這樣不斷重複,從而實現滑動過程。

3.經常使用的滑動實現方法
(1)修改view的left、top、right和bottom的值:調用layout方法或者offsetLeftAndRight等方法
絕對座標系下

// 絕對座標方式
@Override
public boolean onTouchEvent(MotionEvent event) {
int rawX = (int) (event.getRawX());
int rawY = (int) (event.getRawY());
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 記錄觸摸點座標
lastX = rawX;
lastY = rawY;
break;
case MotionEvent.ACTION_MOVE:
// 計算偏移量
int offsetX = rawX - lastX;
int offsetY = rawY - lastY;
// 在當前left、top、right、bottom的基礎上加上偏移量
layout(getLeft() + offsetX,
getTop() + offsetY,
getRight() + offsetX,
getBottom() + offsetY);
// 從新設置初始座標
lastX = rawX;
lastY = rawY;
break;
}
return true;
}

 

視圖座標系下

// 視圖座標方式
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 記錄觸摸點座標
lastX = x;
lastY = y;
break;
case MotionEvent.ACTION_MOVE:
// 計算偏移量
int offsetX = x - lastX;
int offsetY = y - lastY;
// 在當前left、top、right、bottom的基礎上加上偏移量
layout(getLeft() + offsetX,
getTop() + offsetY,
getRight() + offsetX,
getBottom() + offsetY);
//offsetLeftAndRight(offsetX);
//offsetTopAndBottom(offsetY);
break;
}
return true;
}

 

(2)修改佈局參數LayoutParams:修改子view的getLayoutParams或者使用ViewGroup.MarginLayoutParams
子view的getLayoutParams獲得的LayoutParams須要和父ViewGroup的Layout類型一致,若是使用ViewGroup.MarginLayoutParams的話那就方便一些,不須要考慮父ViewGroup的具體類型。

@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 記錄觸摸點座標
lastX = (int) event.getX();
lastY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
// 計算偏移量
int offsetX = x - lastX;
int offsetY = y - lastY;
//ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) getLayoutParams();
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) getLayoutParams();
layoutParams.leftMargin = getLeft() + offsetX;
layoutParams.topMargin = getTop() + offsetY;
setLayoutParams(layoutParams);
break;
}
return true;
}

 

(3)使用scrollTo和scrollBy方法
scrollTo和scrollBy方法移動的是view的content,即讓view的內容移動。若是在ViewGroup中使用scrollTo或者scrollBy方法,那麼移動的是全部子view。但若是在view中使用,那麼移動的將是view的內容,例如TextView,content就是它的文本;ImageView,content就是它的drawable對象。

@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = (int) event.getX();
lastY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
((View) getParent()).scrollBy(-offsetX, -offsetY);//注意這裏須要使用負號進行移動!
break;
}
return true;
}

 

(4)使用Scroller實現平滑效果
前面的滑動都不是平滑的,而Scroller是能夠實現平滑效果的,它的實現原理很簡單,其實就是不斷調用scrollTo和scrollBy方法來實現view的平滑移動,由於人眼的視覺暫留特性看起來就是平滑的。
使用Scroller主要有三個步驟:
1.初始化Scroller對象,通常在view初始化的時候同時初始化scroller;
2.重寫view的computeScroll方法,computeScroll方法是不會自動調用的,只能經過invalidate->draw->computeScroll來間接調用,實現循環獲取scrollX和scrollY的目的,當移動過程結束以後,Scroller.computeScrollOffset方法會返回false,從而中斷循環;
3.調用Scroller.startScroll方法,將起始位置、偏移量以及移動時間(可選)做爲參數傳遞給startScroll方法。

例如,書中給出的例子,子view在被拖動以後會自動平滑移動到原來的位置

private void ininView(Context context) {
setBackgroundColor(Color.BLUE);
// 初始化Scroller
mScroller = new Scroller(context);
}

@Override
public void computeScroll() {
super.computeScroll();
// 判斷Scroller是否執行完畢
if (mScroller.computeScrollOffset()) {
((View) getParent()).scrollTo( mScroller.getCurrX(), mScroller.getCurrY());
// 經過重繪來不斷調用computeScroll
invalidate();//很重要
}
}

@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = (int) event.getX();
lastY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
int offsetX = x - lastX;
int offsetY = y - lastY;
((View) getParent()).scrollBy(-offsetX, -offsetY);
break;
case MotionEvent.ACTION_UP:
// 手指離開時,執行滑動過程
View viewGroup = ((View) getParent());
mScroller.startScroll( viewGroup.getScrollX(), viewGroup.getScrollY(),
-viewGroup.getScrollX(), -viewGroup.getScrollY());
invalidate();//很重要
break;
}
return true;
}

 

(5)屬性動畫 [後面有專門對Android動畫分析的部分,此處略過]

(6)使用ViewDragHelper
ViewDragHelper類使用較少,它是support庫中DrawerLayout和SlidingPaneLayout內部實現的重要類!
以前讀過相似側邊欄菜單的實現代碼(SlidingMenu),我的感受ViewDragHelper實際上是更高層次的封裝,將這類效果所需的接口暴露出來以簡化相似的開發工做,書中給了一個例子,介紹了ViewDragHelper的使用,實現了相似側邊欄菜單的效果

public class DragViewGroup extends FrameLayout {

private ViewDragHelper mViewDragHelper;
private View mMenuView, mMainView;
private int mWidth;

public DragViewGroup(Context context) {
super(context);
initView();
}

public DragViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}

public DragViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}

@Override
protected void onFinishInflate() {
super.onFinishInflate();
mMenuView = getChildAt(0);
mMainView = getChildAt(1);
}

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = mMenuView.getMeasuredWidth();
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return mViewDragHelper.shouldInterceptTouchEvent(ev);
}

@Override
public boolean onTouchEvent(MotionEvent event) {
//將觸摸事件傳遞給ViewDragHelper,此操做必不可少
mViewDragHelper.processTouchEvent(event);
return true;
}

private void initView() {
mViewDragHelper = ViewDragHelper.create(this, callback);
}

private ViewDragHelper.Callback callback = new ViewDragHelper.Callback() {

// 什麼時候開始檢測觸摸事件
@Override
public boolean tryCaptureView(View child, int pointerId) {
//若是當前觸摸的child是mMainView時開始檢測
return mMainView == child;
}

// 觸摸到View後回調
@Override
public void onViewCaptured(View capturedChild, int activePointerId) {
super.onViewCaptured(capturedChild, activePointerId);
}

// 當拖拽狀態改變,好比idle,dragging
@Override
public void onViewDragStateChanged(int state) {
super.onViewDragStateChanged(state);
}

// 當位置改變的時候調用,經常使用與滑動時更改scale等
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
}

// 處理垂直滑動
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return 0;
}

// 處理水平滑動
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return left;
}

// 拖動結束後調用
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
super.onViewReleased(releasedChild, xvel, yvel);
//手指擡起後緩慢移動到指定位置
if (mMainView.getLeft() < 500) {
//關閉菜單,至關於Scroller的startScroll方法
mViewDragHelper.smoothSlideViewTo(mMainView, 0, 0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
} else {
//打開菜單
mViewDragHelper.smoothSlideViewTo(mMainView, 300, 0);
ViewCompat.postInvalidateOnAnimation(DragViewGroup.this);
}
}
};

@Override
public void computeScroll() {
if (mViewDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
}
}

 

OK,本節結束,謝謝閱讀。

相關文章
相關標籤/搜索