有一俩月没写博客了,这惰性没救了 今天补救一下。
不少人在找下拉刷新、上拉加载的控件,其实网上的各种解决方案挺多的,
Github上面更是各种脑洞大开的绚丽效果,不过大多只有下拉刷新比如官方的SwipeRefreshLayout,
或者只支持ListView,再或者是集成很不方便,甚至有些会与自己重写的触摸事件冲突之类的,
今天为大家分享个我自己写的下拉上拉控件,支持ListView ScrollView TextView ImageView WebView,
虽然有点小限制但是我认为对于一般APP来讲影响不大,集成也不是很费事,
不会与自定义的OnTouch事件相冲突。
特别方便为已有项目添加上拉下拉功能,而且代码改动不大。
先放上demo 下载地址
下面直接进入正题
- 先上图 样式就是最普通的样式
集成
拷贝2个xml布局文件到layout文件夹 1. refresh_top_item.xml 头布局文件 2. refresh_bottom_item.xml 尾布局文件 拷贝2个资源图片到 drawable-hdpi文件夹 1. goicon_up.png 2. go icon.png 拷贝3个源码文件到相应包 1. PullableLayout.java 2. HeaderView.java 3. FooterView.java
- 使用方法
xml中PullableLayout直接当做 LinearLayout使用
需要注意的是 如果子布局是 TextView ImageView LinearLayout之类的或者其他默认不可点击的控件需要设置 clickable=”true” ,可以不用实现点击事件。因为如果事件不向子控件分发的话 拦截事件是不会触发的.
<com.example.pullablelayout.widgets.PullableLayout
android:layout_width="match_parent" android:layout_height="match_parent"
android:id="@+id/pullableLayout">
<ScrollView android:layout_width="match_parent" android:layout_height="match_parent">
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"
android:orientation="vertical">
</LinearLayout>
</ScrollView>
</com.example.pullablelayout.widgets.PullableLayout>
activity中设置头尾布局 添加监听事件
pullableLayout = (PullableLayout) findViewById(R.id.pullableLayout);
//设置下拉头
pullableLayout.setHeader(new HeaderView(MainActivity.this));
//设置上拉尾
pullableLayout.setFooter(new FooterView(MainActivity.this));
//设置上拉下拉监听 MainActivity实现了 OnRefreshListener接口
pullableLayout.setRefreshListener(MainActivity.this);
完成监听事件
OnRefreshListener要实现两个方法 下拉 onPullDown() 上拉onPullUp()
/**
* 下拉上拉回调的接口
*/
public interface OnRefreshListener{
/**
* 下拉回调
*/
public void onPullDown();
/**
* 上拉回调
*/
public void onPullUp();
}
在对应的方法里完成异步回调 我这里仅仅是个例子
@Override
public void onPullDown() {
//举个例子 不要在意这些细节
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
pullableLayout.stopRefresh(null);
}
}, 2000);
}
@Override
public void onPullUp() {
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
pullableLayout.stopRefresh(null);
}
}, 2000);
}
好了 大功告成 集成完毕 。
基本原理
如图下拉上拉总体分三部分 就是控制header、footer的显示
header的topMargin设置为 headerview高度*-1
footer的bottomMargin设置为 footerview高度*-1
上拉下拉过程中利用scrollTo方法滚动到相应位置 显示和隐藏对应布局
难点在于事件拦截
实现
PullableLayout < Extends LinearLayout>
集成自LinearLayout 在Xml中完全可以当作 线性布局使用使用到的自定义的变量如下 每个都有注释应该很好理解
public class PullableLayout extends LinearLayout{
public static final int STATUS_NORMAL = 0; //正常状态
public static final int STATUS_DOWN = 1; //下拉状态
public static final int STATUS_UP = 2; //上拉状态
public ExtraView header; //下拉控件
public ExtraView footer; //上拉控件
public int intercepted = 0; //记录单次触摸事件的状态
public int status = STATUS_NORMAL; //记录view当前的状态 下拉刷新中 上拉加载中
private float downX=0,downY=0; //手指按下时的位置
private int startY; //开始上拉 下拉的Y轴位置
public static final String TAG = "PullableLayout";
private OnRefreshListener refreshListener; //事件监听
- 头尾布局类需要实现的接口
具体实现的例子可以看源码 也可以自己实现自定义各种效果
public interface ExtraView{
/**
* 事先定义好的几种状态位
*/
public final static int PULL_To_REFRESH = 0;
public final static int RELEASE_To_REFRESH = 1;
public final static int REFRESHING = 2;
public final static int DONE = 3;
/**
* 获取布局的高度 如果不想隐藏返回0即可
* @return 布局的高度
*/
public int getLayoutHeight();
/**
* 获取布局view
* @return
*/
public View getView();
/**
* 滑动事件的回调
* @param length 计算后的下拉的距离 为了有迟滞感 其实是返回的是实际下拉距离的一半
*/
public void onPull(int length);
/**
*
* @param obj 传递给 header view的参数 即 stopRefresh(Object obj)的参数
*/
public void onFinish(Object obj);
/**
* 松手后开始刷新、加载的回调
*/
public void onRefresh();
/**
* 是否可以刷新
* @return 如果返回false 则不回调 OnRefreshListener
*/
public boolean canRefresh();
}
- 设置头尾布局
/**
* 设置下拉的布局 通过布局的 margin达到隐藏view的目的
* @param view
*/
public void setHeader(ExtraView view){
if(view==null){
return;
}
header = view;
LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT,header.getLayoutHeight());
lp.topMargin = -header.getLayoutHeight();
addView(header.getView(), 0, lp);
}
/**
* 设置上拉布局 通过布局的 margin达到隐藏view的目的
* @param view
*/
public void setFooter(ExtraView view){
if(view==null){
return;
}
footer = view;
LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT,footer.getLayoutHeight());
lp.bottomMargin = -footer.getLayoutHeight();
addView(footer.getView(), getChildCount(), lp);
}
- 事件拦截
//滑动事件拦截 具体处理在 onTouchEvent中
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
//如果没有设置上拉下拉直接返回
if(header==null && footer==null){
return super.onInterceptTouchEvent(ev);
}
float x = ev.getX();
float y = ev.getY();
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
downX = x;
downY = y;
}else if(ev.getAction() == MotionEvent.ACTION_MOVE){
//水平滑动 或者 事件已拦截 则不做额外处理
if( Math.abs(x - downX) > Math.abs(y-downY)*2){
return super.onInterceptTouchEvent(ev);
}
if(intercepted>0){
return true;
}
if(y-downY>0){ //下拉
if(canPullDown()){
intercepted = STATUS_DOWN;
startY = (int)y;
return true;
}
}else{ //上拉
if(canPullUp()){
intercepted = STATUS_UP;
startY = (int)y;
return true;
}
}
}else if(ev.getAction()==MotionEvent.ACTION_UP){
intercepted = 0;
}
return super.onInterceptTouchEvent(ev);
}
- 可上拉下拉时机的判断 这个是重点 具体解释见注释
/**
* 判断是否响应拦截下拉事件
* 不同的子控件判断方式不同需要分别讨论
* 需要特别判断的 一般是 AbsListView子类、ScrollView、WebView
* 这里没具体判断当子控件是webview时候的情况 如有需要请自行添加
* @return
*/
private boolean canPullDown(){
if(header==null) {
return false;
}
View childView = getChildAt(1);
if (childView instanceof AbsListView) { //当子控件为 AbsListView的子类时
if(((AbsListView) childView).getChildCount()<1){
return true;
}
//判断显示的第一项的左上角相对于视图左上角的垂直偏移
int top = ((AbsListView) childView).getChildAt(0).getTop();
//布局的上padding
int pad = ((AbsListView) childView).getListPaddingTop();
if ((Math.abs(top - pad)) < 3
&& ((AbsListView) childView).getFirstVisiblePosition() == 0) {
return true;
} else {
return false;
}
}else if(childView instanceof ScrollView){ //当子控件为 ScrollView的子类时
if (((ScrollView) childView).getScrollY() == 0) {
return true;
} else {
return false;
}
}else{
return true;
}
}
/**
* 判断是否响应拦截上拉事件
* 同{@link #canPullDown()}
* @return
*/
private boolean canPullUp(){
if(footer==null) {
return false;
}
int p = 0; //要判断类型的子控件在布局中的位置 没有上拉为0 否则为1
if(header!=null){
p = 1;
}
View childView = getChildAt(p);
if (childView instanceof AbsListView) {
AbsListView absListView = (AbsListView) childView;
if(absListView.getChildCount()<1){
return true;
}
//列表最后一项的右下角的Y坐标
int bottom = absListView.getChildAt(absListView.getChildCount() -1).getBottom();
//列表项显示的底部Y坐标
int pad = absListView.getBottom() - absListView.getListPaddingBottom();
//bottom位置需要在pad之上 (屏幕上的Y坐标 从上到下依次增大)
if(pad-bottom > -1 && absListView.getLastVisiblePosition() == absListView.getAdapter().getCount()-1){
return true;
}else{
return false;
}
}else if(childView instanceof ScrollView){
ScrollView scrollView = (ScrollView) childView;
//这里比较的是 视图滚动到底部时候的右上角的Y坐标
if (scrollView.getScrollY() >= (scrollView.getChildAt(0).getHeight() - scrollView.getMeasuredHeight())){
return true;
} else {
return false;
}
}else{
return true;
}
}
- onTouchEvent的实现
@Override
public boolean onTouchEvent(MotionEvent event) {
if(event.getAction()==MotionEvent.ACTION_UP){ //手指弹起时
//下拉
if(intercepted == STATUS_DOWN){
//判断是否可以响应下拉
if(header.canRefresh() && status == STATUS_NORMAL){
//当前状态更改为下拉刷新中
status = STATUS_DOWN;
header.onRefresh();
//当有多余的滑动距离时弹回 此时不回调 onPull
setHeader(header.getLayoutHeight(),true);
if(refreshListener!=null ){
refreshListener.onPullDown();
}
}else{
//不响应下拉状态时直接复位
setHeader(0,true);
}
}else if(intercepted == STATUS_UP){ //上拉
//同下拉
if(footer.canRefresh() && status == STATUS_NORMAL){
status = STATUS_UP;
footer.onRefresh();
if(refreshListener!=null ){
refreshListener.onPullUp();
}
setFooter(footer.getLayoutHeight(),true);
}else{
setFooter(0,true);
}
}
//一次滑动操作结束 复位intercepted
intercepted = STATUS_NORMAL;
}else if(event.getAction()==MotionEvent.ACTION_MOVE){ //手指在屏幕上滑动时
int y = (int) event.getY();
//根据滑动距离设置不同的显示状态 同时排除越界
if(intercepted == STATUS_DOWN){
int top = (y-startY)/2;
if(top>0){
setHeader(top,false);
}
}else if(intercepted == STATUS_UP){
int bottom = (startY - y)/2;
if(bottom>0){
setFooter(bottom,false);
}
}
}
return super.onTouchEvent(event);
}
/**
* 设置下拉时的响应
* @param marginTop 下拉的距离
* @param intercept 是否拦截header的响应 true则拦截不调用header.onPull()
* 当上/下拉松手复位footer刷新的时候 intercept = true;
*/
private void setHeader(int marginTop,boolean intercept){
if(header!=null && !intercept){
header.onPull(marginTop);
}
//在这里可以设置不同滑动的响应
scrollTo(0, -marginTop);
}
/**
* 设置上拉时的响应
* @param marginBottom 上拉距离
* @param intercept 是否拦截footer的响应 true拦截 不调用 footer.onPull()
* 当上/下拉松手复位footer加载的时候 intercept = true;
*/
private void setFooter(int marginBottom,boolean intercept){
if(footer!=null && !intercept){
footer.onPull(marginBottom);
}
scrollTo(0, marginBottom);
}