您的位置:首页 > 移动开发 > Android开发

Android自定义控件(二)

2016-03-28 17:03 513 查看
这一篇主要来讲一下自定义控件中的自定义viewgroup,我们以项目中最常用的下拉刷新和加载更多组件为例

简单介绍一下自定义viewgroup时应该怎么做。

分析:下拉刷新和加载更多的原理和步骤

自定义一个viewgroup,将headerview、contentview和footerview从上到下依次布局,然后在初始化的时候

通过Scrooller滚动使得该组件在y轴方向上滚动headerview的高度,这样headerview就被隐藏了。而contentview的

宽度和高度都是match_parent的,因此屏幕上 headerview和footerview就都被隐藏在屏幕之外了。当contentview被

滚动到顶部,如果此时用户继续下拉,那么下拉刷新组件将拦截触摸事件,然后根据用户的触摸事件获取到手指滑动的

y轴距离,并通过scroller将该下拉组件在y轴上滚动手指滑动的距离,实现headerview的显示和隐藏,从而达到下拉的效果

。当用户滑动到最底部时会触发加载更多的操作,此时会通过scroller滚动该下拉刷新组件,将footerview显示出来,实现加载更多

的效果。具体步骤如下:

第一步:初始化View即headerView contentView和footerView
第二步:测量三个view的大小,并计算出viewgroup的大小
第三步:布局,将三个view在界面上布局,按照上中下的顺序
第四步:监听屏幕的触摸事件,判断是否下拉刷新或者加载更多
第五步:触发下拉刷新和加载更多事件执行下拉刷新和加载更多
第六步:下拉刷新和加载更多执行完后的重置操作

示例代码:

自定义的viewgroup

package com.jiao.simpleimageview.view;

import android.content.Context;
import android.graphics.Color;
import android.support.v4.view.MotionEventCompat;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.RotateAnimation;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.Scroller;
import android.widget.TextView;

import com.jiao.simpleimageview.R;
import com.jiao.simpleimageview.listener.OnLoadListener;
import com.jiao.simpleimageview.listener.OnRefreshListener;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
* Created by jiaocg on 2016/3/24.
*/
public abstract class RefreshLayoutBase<T extends View> extends ViewGroup implements
OnScrollListener {

/**
*
*/
protected Scroller mScroller;

/**
* 下拉刷新时显示的header view
*/
protected View mHeaderView;

/**
* 上拉加载更多时显示的footer view
*/
protected View mFooterView;

/**
* 本次触摸滑动y坐标上的偏移量
*/
protected int mYOffset;

/**
* 内容视图, 即用户触摸导致下拉刷新、上拉加载的主视图. 比如ListView, GridView等.
*/
protected T mContentView;

/**
* 最初的滚动位置.第一次布局时滚动header的高度的距离
*/
protected int mInitScrollY = 0;
/**
* 最后一次触摸事件的y轴坐标
*/
protected int mLastY = 0;

/**
* 空闲状态
*/
public static final int STATUS_IDLE = 0;

/**
* 下拉或者上拉状态, 还没有到达可刷新的状态
*/
public static final int STATUS_PULL_TO_REFRESH = 1;

/**
* 下拉或者上拉状态
*/
public static final int STATUS_RELEASE_TO_REFRESH = 2;
/**
* 刷新中
*/
public static final int STATUS_REFRESHING = 3;

/**
* LOADING中
*/
public static final int STATUS_LOADING = 4;

/**
* 当前状态
*/
protected int mCurrentStatus = STATUS_IDLE;

/**
* header中的箭头图标
*/
private ImageView mArrowImageView;
/**
* 箭头是否向上
*/
private boolean isArrowUp;
/**
* header 中的文本标签
*/
private TextView mTipsTextView;
/**
* header中的时间标签
*/
private TextView mTimeTextView;
/**
* header中的进度条
*/
private ProgressBar mProgressBar;
/**
* 屏幕高度
*/
private int mScreenHeight;
/**
* Header 高度
*/
private int mHeaderHeight;
/**
* 下拉刷新监听器
*/
protected OnRefreshListener mOnRefreshListener;
/**
* 加载更多回调
*/
protected OnLoadListener mLoadListener;

/**
* @param context
*/
public RefreshLayoutBase(Context context) {
this(context, null);
}

/**
* @param context
* @param attrs
*/
public RefreshLayoutBase(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

/**
* @param context
* @param attrs
* @param defStyle
*/
public RefreshLayoutBase(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs);

// 初始化Scroller对象
mScroller = new Scroller(context);

// 获取屏幕高度
mScreenHeight = context.getResources().getDisplayMetrics().heightPixels;
// header 的高度为屏幕高度的 1/4
mHeaderHeight = mScreenHeight / 4;

// 初始化整个布局
initLayout(context);
}

/**
* 第一步:初始化整个布局
*
* @param context
*/
private final void initLayout(Context context) {
// header view
setupHeaderView(context);
// 设置内容视图
setupContentView(context);
// 设置布局参数
setDefaultContentLayoutParams();
// 添加mContentView
addView(mContentView);
// footer view
setupFooterView(context);

}

/**
* 初始化 header view
*/
protected 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);
mHeaderView.setPadding(0, mHeaderHeight - 100, 0, 0);
addView(mHeaderView);

// HEADER VIEWS
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);
}

/**
* 初始化Content View, 子类覆写.
*/
protected abstract void setupContentView(Context context);

/**
* 设置Content View的默认布局参数
*/
protected void setDefaultContentLayoutParams() {
ViewGroup.LayoutParams params =
new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT);
mContentView.setLayoutParams(params);
}

/**
* 初始化footer view
*/
protected void setupFooterView(Context context) {
mFooterView = LayoutInflater.from(context).inflate(R.layout.pull_to_refresh_footer,
this, false);
addView(mFooterView);
}

/**
* 第二步:测量
* 丈量视图的宽、高。宽度为用户设置的宽度,高度则为header,
* content view, footer这三个子控件的高度之和。
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int childCount = getChildCount();
int finalHeight = 0;
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
// measure
measureChild(child, widthMeasureSpec, heightMeasureSpec);
// 该view所需要的总高度
finalHeight += child.getMeasuredHeight();
}
setMeasuredDimension(width, finalHeight);
}

/**
* 第三步:布局
* 布局函数,将header, content view,
* footer这三个view从上到下布局。布局完成后通过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();
}

// 计算初始化滑动的y轴距离
mInitScrollY = mHeaderView.getMeasuredHeight() + getPaddingTop();
// 滑动到header view高度的位置, 从而达到隐藏header view的效果
scrollTo(0, mInitScrollY);
}

/**
* 第四步:监听滑动事件
* 与Scroller合作,实现平滑滚动。在该方法中调用Scroller的computeScrollOffset来判断滚动是否结束。
* 如果没有结束,
* 那么滚动到相应的位置,并且调用postInvalidate方法重绘界面,
* 从而再次进入到这个computeScroll流程,直到滚动结束。
*/
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}

/*
* 在适当的时候拦截触摸事件,这里指的适当的时候是当mContentView滑动到顶部,
* 并且是下拉时拦截触摸事件,否则不拦截,交给其child
* view 来处理。
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {

final int action = MotionEventCompat.getActionMasked(ev);
// Always handle the case of the touch gesture being complete.
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
// Do not intercept touch event, let the child handle it
return false;
}

switch (action) {

case MotionEvent.ACTION_DOWN:
mLastY = (int) ev.getRawY();
break;

case MotionEvent.ACTION_MOVE:
// int yDistance = (int) ev.getRawY() - mYDown;
mYOffset = (int) ev.getRawY() - mLastY;
// 如果拉到了顶部, 并且是下拉,则拦截触摸事件,从而转到onTouchEvent来处理下拉刷新事件
if (isTop() && mYOffset > 0) {
return true;
}
break;

}
// Do not intercept touch event, let the child handle it
return false;
}

/**
* 第五步:下拉刷新
* 1、滑动view显示出headerview
* 2、进度条滚动,修改标题内容
* 3、执行下拉刷新监听
* 4、刷新成功或失败后重置:隐藏headerview 修改标题内容
* 在这里处理触摸事件以达到下拉刷新或者上拉自动加载的问题
*
* @see android.view.View#onTouchEvent(android.view.MotionEvent)
*/
@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;
}

/**
* 设置滚动的参数
*
* @param yOffset
*/
private void startScroll(int yOffset) {
mScroller.startScroll(getScrollX(), getScrollY(), 0, yOffset);
invalidate();
}

/**
* y轴上滑动到指定位置
*
* @param distance
* @return
*/
protected void changeScrollY(int distance) {
// 最大值为 scrollY(header 隐藏), 最小值为0 ( header 完全显示).
int curY = getScrollY();
// 下拉
if (distance > 0 && curY - distance > getPaddingTop()) {
scrollBy(0, -distance);
} else if (distance < 0 && curY - distance <= mInitScrollY) {
// 上拉过程
scrollBy(0, -distance);
}

curY = getScrollY();
int slop = mInitScrollY / 2;
//
if (curY > 0 && curY < slop) {
mCurrentStatus = STATUS_RELEASE_TO_REFRESH;
} else if (curY > 0 && curY > slop) {
mCurrentStatus = STATUS_PULL_TO_REFRESH;
}
}

/**
* 旋转箭头图标
*/
protected void rotateHeaderArrow() {

if (mCurrentStatus == STATUS_REFRESHING) {
return;
} else if (mCurrentStatus == STATUS_PULL_TO_REFRESH && !isArrowUp) {
return;
} else if (mCurrentStatus == STATUS_RELEASE_TO_REFRESH && isArrowUp) {
return;
}

mProgressBar.setVisibility(View.GONE);
mArrowImageView.setVisibility(View.VISIBLE);
float pivotX = mArrowImageView.getWidth() / 2f;
float pivotY = mArrowImageView.getHeight() / 2f;
float fromDegrees = 0f;
float toDegrees = 0f;
if (mCurrentStatus == STATUS_PULL_TO_REFRESH) {
fromDegrees = 180f;
toDegrees = 360f;
} else if (mCurrentStatus == STATUS_RELEASE_TO_REFRESH) {
fromDegrees = 0f;
toDegrees = 180f;
}

RotateAnimation animation = new RotateAnimation(fromDegrees, toDegrees, pivotX, pivotY);
animation.setDuration(100);
animation.setFillAfter(true);
mArrowImageView.startAnimation(animation);

if (mCurrentStatus == STATUS_RELEASE_TO_REFRESH) {
isArrowUp = true;
} else {
isArrowUp = false;
}
}

/**
* 根据当前状态修改header view中的文本标签
*/
protected void changeTips() {
if (mCurrentStatus == STATUS_PULL_TO_REFRESH) {
mTipsTextView.setText(R.string.pull_to_refresh_pull_label);
} else if (mCurrentStatus == STATUS_RELEASE_TO_REFRESH) {
mTipsTextView.setText(R.string.pull_to_refresh_release_label);
}
}

/**
* 手指抬起时,根据用户下拉的高度来判断是否是有效的下拉刷新操作。
* 如果下拉的距离超过header view的
* 1/2那么则认为是有效的下拉刷新操作,否则恢复原来的视图状态.
*/
private void changeHeaderViewStaus() {
int curScrollY = getScrollY();
// 超过1/2则认为是有效的下拉刷新, 否则还原
if (curScrollY < mInitScrollY / 2) {
mScroller.startScroll(getScrollX(), curScrollY, 0, mHeaderView.getPaddingTop()
- curScrollY);
mCurrentStatus = STATUS_REFRESHING;
mTipsTextView.setText(R.string.pull_to_refresh_refreshing_label);
mArrowImageView.clearAnimation();
mArrowImageView.setVisibility(View.GONE);
mProgressBar.setVisibility(View.VISIBLE);
} else {
mScroller.startScroll(getScrollX(), curScrollY, 0, mInitScrollY - curScrollY);
mCurrentStatus = STATUS_IDLE;
}

invalidate();
}

/**
* 执行下拉刷新
*/
protected void doRefresh() {
changeHeaderViewStaus();
// 执行刷新操作
if (mCurrentStatus == STATUS_REFRESHING && mOnRefreshListener != null) {
mOnRefreshListener.onRefresh();
}
}

/**
* 刷新结束,恢复状态
*/
public void refreshComplete() {
mScroller.startScroll(getScrollX(), getScrollY(), 0, mInitScrollY - getScrollY());
mCurrentStatus = STATUS_IDLE;
invalidate();
updateHeaderTimeStamp();

// 200毫秒后处理arrow和progressbar,免得太突兀
this.postDelayed(new Runnable() {

@Override
public void run() {
mArrowImageView.setVisibility(View.VISIBLE);
mProgressBar.setVisibility(View.GONE);
}
}, 100);

}

/**
* 修改header上的最近更新时间
*/
private void updateHeaderTimeStamp() {
// 设置更新时间
mTimeTextView.setText(R.string.pull_to_refresh_update_time_label);
SimpleDateFormat sdf = (SimpleDateFormat) SimpleDateFormat.getInstance();
sdf.applyPattern("yyyy-MM-dd HH:mm:ss");
mTimeTextView.append(sdf.format(new Date()));
}

/**
* 第六步:加载更多
* 滚动监听,当滚动到最底部,且用户设置了加载更多的监听器时触发加载更多操作.
* AbsListView, int, int, int)
*/
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
int totalItemCount) {
// 用户设置了加载更多监听器,且到了最底部,并且是上拉操作,那么执行加载更多.
if (mLoadListener != null && isBottom() && mScroller.getCurrY() <= mInitScrollY
&& mYOffset <= 0
&& mCurrentStatus == STATUS_IDLE) {
showFooterView();
doLoadMore();
}
}

@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {

}

/**
* 执行下拉(自动)加载更多的操作
*/
protected void doLoadMore() {
if (mLoadListener != null) {
mLoadListener.onLoadMore();
}
}
/**
* 显示footer view
*/
private void showFooterView() {
startScroll(mFooterView.getMeasuredHeight());
mCurrentStatus = STATUS_LOADING;
}

/**
* 加载结束,恢复状态
*/
public void loadCompelte() {
// 隐藏footer
startScroll(mInitScrollY - getScrollY());
mCurrentStatus = STATUS_IDLE;
}

/**
* 设置下拉刷新监听器
*
* @param listener
*/
public void setOnRefreshListener(OnRefreshListener listener) {
mOnRefreshListener = listener;
}

/**
* 设置滑动到底部时自动加载更多的监听器
*
* @param listener
*/
public void setOnLoadListener(OnLoadListener listener) {
mLoadListener = listener;
}

/**
* 是否已经到了最顶部,子类需覆写该方法,使得mContentView滑动到最顶端时返回true, 如果到达最顶端用户继续下拉则拦截事件;
*
* @return
*/
protected abstract boolean isTop();

/**
* 是否已经到了最底部,子类需覆写该方法,使得mContentView滑动到最底端时返回true;从而触发自动加载更多的操作
*
* @return
*/
protected abstract boolean isBottom();

/**
* 返回Content View
*
* @return
*/
public T getContentView() {
return mContentView;
}

/**
* @return
*/
public View getHeaderView() {
return mHeaderView;
}

/**
* @return
*/
public View getFooterView() {
return mFooterView;
}

}


实现下拉刷新的listview

package com.jiao.simpleimageview.view;

import android.content.Context;
import android.util.AttributeSet;
import android.widget.ListAdapter;
import android.widget.ListView;

/**
* Created by jiaocg on 2016/3/25.
*/
public class RefreshListView extends RefreshLayoutBase<ListView> {
/**
* @param context
*/
public RefreshListView(Context context) {
this(context, null);
}

/**
* @param context
* @param attrs
*/
public RefreshListView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

/**
* @param context
* @param attrs
* @param defStyle
*/
public RefreshListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}

@Override
protected void setupContentView(Context context) {
mContentView = new ListView(context);
// 设置滚动监听器
mContentView.setOnScrollListener(this);

}

@Override
protected boolean isTop() {

//当第一个可见项是第一项时表示已经拉倒了顶部
return mContentView.getFirstVisiblePosition() == 0
&& getScrollY() <= mHeaderView.getMeasuredHeight();
}

@Override
protected boolean isBottom() {
//当最后一个可见项是最后一项时表示已经拉倒了底部
return mContentView != null && mContentView.getAdapter() != null
&& mContentView.getLastVisiblePosition() ==
mContentView.getAdapter().getCount() - 1;
}

/**
* 设置adapter
*/
public void setAdapter(ListAdapter adapter) {
mContentView.setAdapter(adapter);
}

public ListAdapter getAdapter() {
return mContentView.getAdapter();
}

}


然后直接在xml文件中引用使用即可实现,另外这种方式的下拉刷新扩展性很强

也可以实现TextView和GridView的刷新,只需继承该base实现其中的抽象方法即可

源码下载:https://yunpan.cn/cqKRSr2r2MsEk 提取密码:d177
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: