前言
涉及知識點:
- Scroller 使用
- 自定義 ViewGroup
- Measure 測量 View
- onTouchEvent 函數(shù)
初始化視圖
public abstract class RefreshLayoutBase<T extends View> extends ViewGroup implements AbsListView.OnScrollListener{
protected Scroller mScroller;//滾動控制
protected View mHeaderView;
protected View mFooterView;
protected T mContentView;
protected int mYOffset;//本次觸摸 Y 軸坐標的偏移量
protected int mInitScrollY = 0;//最初的滾動位置,第一次布局時滾動header的高度的距離
protected int mLastY = 0;//最后一次觸摸 Y 軸坐標
public static final int STATUS_IDLE = 0;//空閑狀態(tài)
public static final int STATUS_PULL_TO_REFRESH = 1;//下拉或者上拉侣滩,但是沒有達到可以刷新的狀態(tài)
public static final int STATUS_RELEASE_TO_REFRESH = 2;//下拉或者上拉狀態(tài)
public static final int STATUS_REFRESHING = 3;//刷新中
public static final int STATUS_LOADING = 4;//加載中
protected int mCurrentStatus = STATUS_IDLE;
private ImageView mArrowImageView;//header中的箭頭圖標
private boolean isArrowUp;//箭頭是否向上
private TextView mTipsTextView;//header 中的文本提示
private TextView mTimeTextView;//header 中的時間提示
private ProgressBar mProgressBar;//進度條
private int mScreenHeight;//屏幕高度
private int mHeaderHeight;// Header 高度
protected OnRefreshListener mOnRefreshListener;//下拉刷新監(jiān)聽
protected OnLoadListener mLoadListener;//加載更多回調(diào)
public RefreshLayoutBase(Context context) {
this(context, null);
}
public RefreshLayoutBase(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RefreshLayoutBase(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs);
mScroller = new Scroller(context);
mScreenHeight = context.getResources().getDisplayMetrics().heightPixels;//獲取屏幕高度
mHeaderHeight = mScreenHeight / 4;//header 高度為屏幕高度四分之一
initLayout(context);
}
protected final void initLayout(Context context){
//headerView
setupHeaderView(context);
//設置內(nèi)容視圖
setupContentView(context);
//設置布局參數(shù)
setDefaultContentLayoutParams();
//添加 contentView 布局
addView(mContentView);
//設置底部視圖
setupFooterView(context);
}
/**
* 設置布局參數(shù)
* 給 ContentView 寬高設置為 match_parent
*/
private void setDefaultContentLayoutParams() {
ViewGroup.LayoutParams params =
new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT);
mContentView.setLayoutParams(params);
}
/**
* 初始化 footerView
* @param context
*/
private void setupFooterView(Context context) {
mFooterView = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_footer,
this, false);
addView(mFooterView);
}
/**
* 內(nèi)容視圖
*/
protected abstract void setupContentView(Context context);
/**
* 初始化 header
* @param context
*/
private void setupHeaderView(Context context) {
mHeaderView = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_header, this, false);
mHeaderView.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, mHeaderHeight));
mHeaderView.setBackgroundColor(Color.RED);
//header 高度為屏幕 1/4,但是慢叨,他只有 100px 的有效顯示區(qū)域
mHeaderView.setPadding(0, mHeaderHeight - 100, 0, 0);//左上右下
addView(mHeaderView);
mArrowImageView = (ImageView) mHeaderView.findViewById(R.id.pull_to_arrow_image);
mTipsTextView = (TextView) mHeaderView.findViewById(R.id.pull_to_refresh_text);
mTimeTextView = (TextView) mHeaderView.findViewById(R.id.pull_to_refresh_updated_at);
mProgressBar = (ProgressBar) mHeaderView.findViewById(R.id.pull_to_refresh_progress);
}
/**if isTop return true
* 達到頂部繼續(xù)下拉則攔截事件
* @return
*/
protected abstract boolean isTop();
/**
* if isBottom return true
* 達到底部觸發(fā)加載更多
* @return
*/
protected abstract boolean isBottom();
/***************************************************
省略部分代碼壮锻,詳情見github
****************************************************/
}
首先是一大堆的變量,都加了注釋也沒什么說的蟆炊,后面用到再說吧阿趁。這里主要是 initLayout() 初始化了整個下拉刷新的布局膜蛔,從上到下 headerView、contentView脖阵、footerView皂股。其中 header 和 footer 都是從布局中加載的,固定好了的命黔。但是 contentView 是抽象的呜呐,可變的就斤,我們可以設置他是 ListView,RecyclerView蘑辑,GridView 等洋机,到時候繼承即可。
另外還有兩個函數(shù) isTop() 和 isBottom()洋魂,因為我們在下拉刷新和上拉加載更多的時候绷旗,不同的內(nèi)容視圖判斷到達底部頂部的代碼是不一樣的,所以把它也抽象了副砍。
測量寬高
MeasureSpec 的含義衔肢,組成
接下來要做的就是測量。View的測量師自定義View中最重要的一步豁翎。在貼代碼之前角骤,有幾個基本概念要搞清楚⌒陌看圖:
UNSPECIFIED 其實是開發(fā)人員按照自己的意愿調(diào)整大小邦尊,沒有任何限制。但是這種情況很少見刘陶。
EXACTLY 往往對應 match_parent, AT_MOST 往往對應 warp_content
getMeasureHeight() 和 getHeight() 區(qū)別
簡單地說 getMeasureHeight 可以測量到屏幕以外的布局胳赌, getHeight 測量到可視布局牢撼。
舉個例子匙隔,一個上拉加載更多的組件,contentView 是100dp熏版,footerView 是 50dp纷责,但是 footerView(加載更多視圖) 不可見。用getMeasureHeight 測量的高度就是 150dp撼短。
/**
* 測量 viewGroup 寬高再膳。寬度為用戶定義。高度是 header, contentView, footer 三者之和
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int childCount = getChildCount();//子視圖個數(shù)
int finalHeight = 0;//最終的高度
for(int i = 0; i < childCount; i++){
View child = getChildAt(i);
measureChild(child, widthMeasureSpec, heightMeasureSpec);//測量每個子視圖
finalHeight += child.getMeasuredHeight();
}
//設置下拉刷新組件的尺寸(也就是這個 ViewGroup )
setMeasuredDimension(width, finalHeight);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
/**
* 將 header曲横、content喂柒、footer 從上到下布局
* 布局完成后通過 Scroller 滾動到 header 的底部
* 滑動距離為 header 高度 + 本視圖 paddingtop,達到隱藏 header
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childCount = getChildCount();
int top = getPaddingTop();
for(int i=0; i<childCount; i++) {
View child = getChildAt(i);
child.layout(0, top, child.getMeasuredWidth(), child.getMeasuredHeight() + top);
top += child.getMeasuredHeight();
}
mInitScrollY = mHeaderView.getMeasuredHeight() + getPaddingTop();
scrollTo(0, mInitScrollY);
}
在onMeasure中測量了這個組件的自身大小和子視圖的大小禾嫉,并在onLayout中從上到下依次布局灾杰。
在 OnLayout 的最后通過 Scroller 將該 ViewGroup 滑動了 HeaderView 的高度使其不可見。
下拉刷新
當用戶向下按的時候熙参,判斷 ContentView 視圖滑動到了頂部艳吠。此時又通過 Scroller 將該組件向下滾動,使得 HeaderView 可見孽椰。這些功能都需要讓我們處理觸摸事件昭娩。
/**
* 攔截觸摸事件
* 在 ContentView 滑動到頂部凛篙,并且下拉的時候攔截
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//獲取觸摸事件類型
final int action = MotionEventCompat.getActionMasked(ev);
//取消事件或者抬起事件直接返回false
if(action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP){
return false;
}
switch(action) {
case MotionEvent.ACTION_DOWN:
mLastY = (int) ev.getRawY();
break;
case MotionEvent.ACTION_MOVE:
mYOffset = (int) (ev.getRawY() - mLastY);
//如果拉到了頂部,并且是下拉栏渺,攔截事件呛梆,轉(zhuǎn)到 onTouchEvent 處理下拉刷新
if(isTop() && mYOffset > 0){
return true;
}
break;
default:
break;
}
return false;//false 默認不攔截
}
onInterceptTouchEvent 是ViewGroup 中對觸摸事件進行攔截的函數(shù),返回 true
時表示攔截磕诊。
例如:如果 mYOffset > 0削彬,那么代表用戶是從上往下滑動。如果此時 ContentView 已經(jīng)滑動到了頂部秀仲,那么第一個可見元素就是第一項融痛,返回 true
就是將后續(xù)的時間進行攔截。此時神僵,后續(xù)的 ACTION_MOVE 就會轉(zhuǎn)到 onTouchEvent 函數(shù)進行處理雁刷。
/**
* 在這里處理下拉刷新或者上拉加載更多
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
switch(event.getAction()){
case MotionEvent.ACTION_MOVE:
int currentY = (int) event.getRawY();
mYOffset = currentY - mLastY;
if(mCurrentStatus != STATUS_LOADING){
changeScrollY(mYOffset);
}
rotateHeaderArrow();
changeTips();
mLastY = currentY;
break;
case MotionEvent.ACTION_UP:
//下拉刷新具體操作
doRefresh();
break;
default:
break;
}
return true;//返回 true,消費該事件
}
protected void doRefresh() {
changeHeaderViewStaus();
if(mCurrentStatus == STATUS_REFRESHING && mOnRefreshListener != null){
mOnRefreshListener.onRefresh();
}
}
在 onTouchEvent 函數(shù)中保礼,判斷他的事件類型沛励。如果還是 MOVE,就計算 y 坐標的差值炮障,在此處調(diào)用 changeScrollY 函數(shù)目派,在 y 軸上滾動該控件,另外還有改變箭頭方向胁赢,文字提示等企蹭。如果是 ACTION_UP 手指抬起,說明松手了智末,就執(zhí)行下拉刷新操作谅摄。當然執(zhí)行的時候要判斷 y坐標下拉的偏移值夠不夠。
/**
* 刷新結(jié)束時候調(diào)用系馆,視圖還原為基本狀態(tài)
*/
public void refreshComplete(){
mScroller.startScroll(getScrollX(),getScrollY(),0,mInitScrollY - getScrollY());
mCurrentStatus = STATUS_IDLE;
invalidate();
updateHeaderTimeStamp();
//100毫秒之后處理arrow和progress送漠,免得太突兀
this.postDelayed(new Runnable() {
@Override
public void run() {
mArrowImageView.setVisibility(View.VISIBLE);
mProgressBar.setVisibility(View.GONE);
}
},100);
}
本文僅僅是自己探究一下。其實個人覺得由蘑, MD 的 SwipeRefreshLayout 漂亮好用多了/手動滑稽