你的位置:首页 > 操作系统

[操作系统]高扩展自定义下拉控件

最近写了个下拉控件,和几个下拉的头部样式,下拉控件可以连续添加叠加几个头部视图

下面是没有添加任何头部尾部的视图下拉效果

一步一步来介绍,先介绍这个下拉效果,在介绍自定义的头部

首先在使用上,和普通的控件没有两样,拿recyclerview来做例子,因为recyclerview使用比较多,而且可以替代很多的列表控件

<??><LinearLayout ="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" ="http://schemas.android.com/apk/res-auto" android:orientation="vertical"> <com.fragmentapp.view.refresh.RefreshLayout  android:layout_width="match_parent"  android:layout_height="match_parent"  app:_height="@dimen/dp_60"  android:id="@+id/refreshLayout" >  <android.support.v7.widget.RecyclerView   android:id="@+id/recyclerView"   android:divider="@color/color_e1e1e1"   android:dividerHeight="10dp"   android:background="#ffffff"   android:layout_width="match_parent"   android:layout_height="match_parent" /> </com.fragmentapp.view.refresh.RefreshLayout></LinearLayout>
protected void init() {  for (int i = 0; i < 20; i++) {   list.add("" + i);  }  recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));  recyclerView.setAdapter(new HomeAdapter(getActivity(), R.layout.item_home, list));  refreshLayout    .setCallBack(new RefreshLayout.CallBack() {     @Override     public void refreshHeaderView(int state, String stateVal) {      switch (state) {       case RefreshLayout.DOWN_REFRESH: // 下拉刷新状态        break;       case RefreshLayout.RELEASE_REFRESH: // 松开刷新状态        break;       case RefreshLayout.LOADING: // 正在刷新中状态        break;      }     }     @Override     public void pullListener(int y) {     }    }); }
 1 package com.fragmentapp.view.refresh; 2  3 import android.content.Context; 4 import android.content.res.TypedArray; 5 import android.graphics.Color; 6 import android.graphics.Rect; 7 import android.support.v4.view.ViewCompat; 8 import android.support.v7.widget.LinearLayoutManager; 9 import android.support.v7.widget.RecyclerView; 10 import android.support.v7.widget.StaggeredGridLayoutManager; 11 import android.util.AttributeSet; 12 import android.util.Log; 13 import android.view.Gravity; 14 import android.view.MotionEvent; 15 import android.view.View; 16 import android.view.ViewConfiguration; 17 import android.widget.AbsListView; 18 import android.widget.FrameLayout; 19  20 import com.fragmentapp.R; 21  22 import java.lang.reflect.Field; 23 import java.util.ArrayList; 24 import java.util.List; 25  26 /** 27  * Created by LiuZhen on 2017/3/24. 28 */ 29 public class RefreshLayout extends FrameLayout { 30  31  private String TAG = "tag"; 32  private int downY;// 按下时y轴的偏移量 33  private final static float RATIO = 3f; 34  //头部的高度 35  protected int mHeadHeight = 120; 36  //头部layout 37  protected FrameLayout mHeadLayout,mFootLayout;//头部容器 38  private List<IHeadView> heads = new ArrayList<>();//支持添加多个头部 39  private List<IFootView> foots = new ArrayList<>(); 40  41  public static final int DOWN_REFRESH = 0;// 下拉刷新状态 42  public static final int RELEASE_REFRESH = 1;// 松开刷新 43  public static final int LOADING = 2;// 正在刷新中 44  private int currentState = DOWN_REFRESH;// 头布局的状态: 默认为下拉刷新状态 45  46  private View list;//子节点中的 recyclerview 视图 47  private LayoutParams listParam,footParam;//用于控制下拉动画展示 48  private boolean isLoadingMore = false;// 是否进入加载状态,防止多次重复的启动 49  private boolean isStart = false;//表示正在加载刷新中,还没停止 50  private boolean isTop = false,isBottom = false; 51  private int mTouchSlop; 52  private CallBack callBack; 53  54  public RefreshLayout(Context context) { 55   this(context, null, 0); 56  } 57  58  public RefreshLayout(Context context, AttributeSet attrs) { 59   this(context, attrs, 0); 60  } 61  62  public RefreshLayout(Context context, AttributeSet attrs, int defStyleAttr) { 63   super(context, attrs, defStyleAttr); 64  65   TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RefreshLayout, defStyleAttr, 0); 66   try { 67    mHeadHeight = a.getDimensionPixelSize(R.styleable.RefreshLayout__height, 120); 68   } finally { 69    a.recycle(); 70   } 71   mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); 72  73   init(); 74  } 75  76  private void initHeaderContainer() { 77   mHeadLayout = new FrameLayout(getContext()); 78   LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,mHeadHeight); 79   this.addView(mHeadLayout,layoutParams); 80  } 81  82  public void initFootContainer() { 83   footParam = new LayoutParams(LayoutParams.MATCH_PARENT,mHeadHeight); 84   mFootLayout = new FrameLayout(getContext());//底部布局 85   mFootLayout.setBackgroundColor(Color.BLACK); 86   footParam.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; 87   footParam.setMargins(0,0,0,-mHeadHeight); 88   this.addView(mFootLayout,footParam); 89  } 90  91  private void init(){ 92   initHeaderContainer(); 93  } 94  95  @Override 96  protected void onFinishInflate() {//布局加载成 97   super.onFinishInflate(); 98  99   initFootContainer();100   if (list == null) {101    list = getChildAt(1);102    listParam = (LayoutParams) list.getLayoutParams();103    list.setOnTouchListener(new OnTouchListener() {104     @Override105     public boolean onTouch(View v, MotionEvent event) {106      return onFingerTouch(event);107     }108    });109   }110  }111 112  /**113   * 设置头部View114  */115  public RefreshLayout setHeaderView(final IHeadView headView) {116   if (headView != null) {117    mHeadLayout.addView(headView.getView());118    heads.add(headView);119   }120   return this;121  }122 123  /**124   * 设置尾部View125  */126  public RefreshLayout setFootView(final IFootView footView) {127   if (footView != null) {128    mFootLayout.addView(footView.getView());129    foots.add(footView);130   }131   return this;132  }133 134  public boolean onFingerTouch(MotionEvent ev) {135   isTop = isViewToTop(list,mTouchSlop);136   isBottom = isViewToBottom(list,mTouchSlop);137 //  Log.e(TAG,"isTop "+isTop+" isBottom "+isBottom);138   switch (ev.getAction()) {139    case MotionEvent.ACTION_DOWN :140     currentState = LOADING;141     downY = (int) ev.getY();142     break;143    case MotionEvent.ACTION_MOVE :144     if (!isTop && !isBottom)//没有到顶,无需计算操作145      break;146     int moveY = (int) ev.getY();147     int diff = (int) (((float)moveY - (float)downY) / RATIO);148 //    int paddingTop = -mHeadLayout.getHeight() + diff;149     int paddingTop = diff;150     if (paddingTop>0 && isTop) {151      //向下滑动多少后开始启动刷新,Margin判断是为了限制快速用力滑动的时候导致头部侵入的高度不够就开始加载了152      if (paddingTop >= mHeadHeight && (listParam.topMargin >= mHeadHeight) && currentState == DOWN_REFRESH) { // 完全显示了.153 //      Log.i(TAG, "松开刷新 RELEASE_REFRESH");154       currentState = RELEASE_REFRESH;155       refreshHeaderView();156       start();157      } else if (currentState == LOADING) { // 没有显示完全158 //      Log.i(TAG, "下拉刷新 DOWN_PULL_REFRESH");159       currentState = DOWN_REFRESH;160       refreshHeaderView();161      }162      if (paddingTop <= (mHeadHeight+10) && !isStart) {//已经处于运行刷新状态的时候禁止设置163       listParam.setMargins(0, paddingTop, 0, 0);164       list.setLayoutParams(listParam);165       if (callBack != null)166        callBack.pullListener(paddingTop);167      }168 169     }else if (isBottom){170      //限制上滑时不能超过底部的宽度,不然会超出边界171      //mHeadHeight+20 上滑设置的margin要超过headheight,不然下面判断的大于headheight不成立,下面的margin基础上面设置后的参数172      if (Math.abs(paddingTop) <= (mHeadHeight+10) && !isStart) {//已经处于运行刷新状态的时候禁止设置173       listParam.setMargins(0, 0, 0, -paddingTop);174       footParam.setMargins(0,0,0,-paddingTop-mHeadHeight);175       list.setLayoutParams(listParam);176      }177      //如果滑动的距离大于头部或者底部的高度,并且设置的margin也大于headheight178      //listParam用来限制recyclerview列表迅速滑动,footParam用来限制bottom foothead迅速滑动导致没有达到head的高度就开始加载了179      if (Math.abs(paddingTop) >= mHeadHeight && (listParam.bottomMargin >= mHeadHeight || footParam.bottomMargin >= 0))180       isLoadingMore = true;//头部是否拉取到位,然后执行加载动画181 182     }183 //    Log.e(TAG,"paddingTop "+paddingTop +" mHeadHeight "+mHeadHeight+ " topMargin "+listParam.topMargin+" bottomMargin "+listParam.bottomMargin184 //      +" footParam bottom "+footParam.bottomMargin);185 //    Log.i(TAG,"paddingTop "+paddingTop);186     break;187    case MotionEvent.ACTION_UP :188     currentState = LOADING;189     refreshHeaderView();190     if (isLoadingMore){191      isLoadingMore = false;192      isStart = true;//是否开始加载193      postDelayed(new Runnable() {194       @Override195       public void run() {196 //       Log.i(TAG, "停止 END");197 //       currentState = END;198        refreshHeaderView();199        listParam.setMargins(0, 0, 0, 0);200        footParam.setMargins(0,0,0,-mHeadHeight);201        list.setLayoutParams(listParam);202        stop();203       }204      },2000);205     }else{206      if (!isStart){207       // 隐藏头布局208       listParam.setMargins(0, 0,0,0);209       footParam.setMargins(0,0,0,-mHeadHeight);210       list.setLayoutParams(listParam);211      }212     }213 //    Log.i(TAG, "松开 REFRESHING");214     break;215    default :216     break;217   }218   return super.onTouchEvent(ev);219  }220 221  /**222   * 根据currentState刷新头布局的状态223  */224  private void refreshHeaderView() {225   if (callBack == null || isStart)226    return;227   String val = "准备刷新";228   switch (currentState) {229    case DOWN_REFRESH : // 下拉刷新状态230     val = "下拉刷新";231     break;232    case RELEASE_REFRESH : // 松开刷新状态233     val = "开始刷新...";234     break;235    case LOADING : // 正在刷新中状态236     val = "正在刷新中...";237     break;238   }239   callBack.refreshHeaderView(currentState,val);240  }241 242  public static boolean isViewToTop(View view, int mTouchSlop){243   if (view instanceof AbsListView) return isAbsListViewToTop((AbsListView) view);244   if (view instanceof RecyclerView) return isRecyclerViewToTop((RecyclerView) view);245   return (view != null && Math.abs(view.getScrollY()) <= 2 * mTouchSlop);246  }247 248  public static boolean isViewToBottom(View view, int mTouchSlop){249   if (view instanceof AbsListView) return isAbsListViewToBottom((AbsListView) view);250   if (view instanceof RecyclerView) return isRecyclerViewToBottom((RecyclerView) view);251 //  if (view instanceof WebView) return isWebViewToBottom((WebView) view,mTouchSlop);252 //  if (view instanceof ViewGroup) return isViewGroupToBottom((ViewGroup) view);253   return false;254  }255 256  public static boolean isAbsListViewToTop(AbsListView absListView) {257   if (absListView != null) {258    int firstChildTop = 0;259    if (absListView.getChildCount() > 0) {260     // 如果AdapterView的子控件数量不为0,获取第一个子控件的top261     firstChildTop = absListView.getChildAt(0).getTop() - absListView.getPaddingTop();262    }263    if (absListView.getFirstVisiblePosition() == 0 && firstChildTop == 0) {264     return true;265    }266   }267   return false;268  }269 270  public static boolean isRecyclerViewToTop(RecyclerView recyclerView) {271   if (recyclerView != null) {272    RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();273    if (manager == null) {274     return true;275    }276    if (manager.getItemCount() == 0) {277     return true;278    }279 280    if (manager instanceof LinearLayoutManager) {281     LinearLayoutManager layoutManager = (LinearLayoutManager) manager;282 283     int firstChildTop = 0;284     if (recyclerView.getChildCount() > 0) {285      // 处理item高度超过一屏幕时的情况286      View firstVisibleChild = recyclerView.getChildAt(0);287      if (firstVisibleChild != null && firstVisibleChild.getMeasuredHeight() >= recyclerView.getMeasuredHeight()) {288       if (android.os.Build.VERSION.SDK_INT < 14) {289        return !(ViewCompat.canScrollVertically(recyclerView, -1) || recyclerView.getScrollY() > 0);290       } else {291        return !ViewCompat.canScrollVertically(recyclerView, -1);292       }293      }294 295      // 如果RecyclerView的子控件数量不为0,获取第一个子控件的top296 297      // 解决item的topMargin不为0时不能触发下拉刷新298      View firstChild = recyclerView.getChildAt(0);299      RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) firstChild.getLayoutParams();300      firstChildTop = firstChild.getTop() - layoutParams.topMargin - getRecyclerViewItemTopInset(layoutParams) - recyclerView.getPaddingTop();301     }302 303     if (layoutManager.findFirstCompletelyVisibleItemPosition() < 1 && firstChildTop == 0) {304      return true;305     }306    } else if (manager instanceof StaggeredGridLayoutManager) {307     StaggeredGridLayoutManager layoutManager = (StaggeredGridLayoutManager) manager;308 309     int[] out = layoutManager.findFirstCompletelyVisibleItemPositions(null);310     if (out[0] < 1) {311      return true;312     }313    }314   }315   return false;316  }317 318  /**319   * 通过反射获取RecyclerView的item的topInset320  */321  private static int getRecyclerViewItemTopInset(RecyclerView.LayoutParams layoutParams) {322   try {323    Field field = RecyclerView.LayoutParams.class.getDeclaredField("mDecorInsets");324    field.setAccessible(true);325    // 开发者自定义的滚动监听器326    Rect decorInsets = (Rect) field.get(layoutParams);327    return decorInsets.top;328   } catch (Exception e) {329    e.printStackTrace();330   }331   return 0;332  }333 334  public static boolean isAbsListViewToBottom(AbsListView absListView) {335   if (absListView != null && absListView.getAdapter() != null && absListView.getChildCount() > 0 && absListView.getLastVisiblePosition() == absListView.getAdapter().getCount() - 1) {336    View lastChild = absListView.getChildAt(absListView.getChildCount() - 1);337 338    return lastChild.getBottom() <= absListView.getMeasuredHeight();339   }340   return false;341  }342 343  public static boolean isRecyclerViewToBottom(RecyclerView recyclerView) {344   if (recyclerView != null) {345    RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();346    if (manager == null || manager.getItemCount() == 0) {347     return false;348    }349 350    if (manager instanceof LinearLayoutManager) {351     // 处理item高度超过一屏幕时的情况352     View lastVisibleChild = recyclerView.getChildAt(recyclerView.getChildCount() - 1);353     if (lastVisibleChild != null && lastVisibleChild.getMeasuredHeight() >= recyclerView.getMeasuredHeight()) {354      if (android.os.Build.VERSION.SDK_INT < 14) {355       return !(ViewCompat.canScrollVertically(recyclerView, 1) || recyclerView.getScrollY() < 0);356      } else {357       return !ViewCompat.canScrollVertically(recyclerView, 1);358      }359     }360 361     LinearLayoutManager layoutManager = (LinearLayoutManager) manager;362     if (layoutManager.findLastCompletelyVisibleItemPosition() == layoutManager.getItemCount() - 1) {363      return true;364     }365    } else if (manager instanceof StaggeredGridLayoutManager) {366     StaggeredGridLayoutManager layoutManager = (StaggeredGridLayoutManager) manager;367 368     int[] out = layoutManager.findLastCompletelyVisibleItemPositions(null);369     int lastPosition = layoutManager.getItemCount() - 1;370     for (int position : out) {371      if (position == lastPosition) {372       return true;373      }374     }375    }376   }377   return false;378  }379 380  public void start(){381   isLoadingMore = true;382   for (IHeadView head : heads) {383    head.startAnim();384   }385  }386 387  public void stop(){388   isLoadingMore = false;389   isStart = false;390   for (IHeadView head : heads) {391    head.stopAnim();392   }393  }394 395  public RefreshLayout setCallBack(CallBack callBack) {396   this.callBack = callBack;397   return this;398  }399 400  public interface CallBack{401   /**监听下拉时的状态*/402   void refreshHeaderView(int state,String stateVal);403   /**监听下拉时的距离*/404   void pullListener(int y);405  }406 407 408 }
View Code

注释估计也没有我写的这么详细的了,相信不是完全不懂的话肯定一眼就看懂了

至于 refreshLayout 这个下拉控件里面的代码是完整的,没有去截取,不过写了非常非常详细的注释,然后控件里面的添加头部视图是通过接口来控制添加的,外加对外提供的状态回调,建造者模式返回本身对象,这样方便扩展

然后现在开始定义头部效果,说到头部效果,扇形的下拉回弹首选了,因为已经有很多例子了,所以我也模仿做了一个,还有一个原因是为了后面的头部做铺垫,叠加头部,那么最先添加的头部肯定要有当背景的觉悟了,先看效果图

 

 上代码,同样是非常非常详细的注释

package com.fragmentapp.view.refresh;import android.animation.ValueAnimator;import android.content.Context;import android.graphics.Canvas;import android.graphics.Paint;import android.graphics.Path;import android.util.AttributeSet;import android.view.View;import android.view.animation.DecelerateInterpolator;import android.view.animation.OvershootInterpolator;import com.fragmentapp.R;/** * Created by liuzhen on 2017/11/29. */public class DownHeadView extends View implements IHeadView{ private int pull_height; private Path mPath; private Paint mBackPaint; private int backColor; private int mWidth; private int mHeight; private DecelerateInterpolator decelerateInterpolator = new DecelerateInterpolator(10); public DownHeadView(Context context) {  this(context, null); } public DownHeadView(Context context, AttributeSet attrs) {  this(context, attrs, 0); } public DownHeadView(Context context, AttributeSet attrs, int defStyleAttr) {  super(context, attrs, defStyleAttr);  init(); } private void init(){  setWillNotDraw(false);  backColor = getResources().getColor(R.color.color_8b90af);  mPath = new Path();  mBackPaint = new Paint();  mBackPaint.setAntiAlias(true);  mBackPaint.setStyle(Paint.Style.FILL);  mBackPaint.setColor(backColor); } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) {  super.onLayout(changed, left, top, right, bottom);  if (changed) {   mWidth = getWidth();   mHeight = getHeight();  } } @Override protected void onDraw(Canvas canvas) {  canvas.drawRect(0, 0, mWidth, pull_height, mBackPaint);  mPath.reset();  mPath.moveTo(0, pull_height);//起点  mPath.quadTo(mWidth/2,pull_height*2,mWidth,pull_height);//控制点和终点  canvas.drawPath(mPath, mBackPaint);//绘制二级贝塞尔弧形  invalidate(); } @Override public View getView() {  return this; } @Override public void startAnim() {  backColor = getResources().getColor(R.color.color_8babaf);  mBackPaint.setColor(backColor);  ValueAnimator va = ValueAnimator.ofFloat(mHeight, mHeight/2);  va.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {   @Override   public void onAnimationUpdate(ValueAnimator animation) {    float val = (float) animation.getAnimatedValue();    val = decelerateInterpolator.getInterpolation(val / mHeight) * val;    pull_height = (int)val;    requestLayout();   }  });  va.setInterpolator(new OvershootInterpolator(3));//甩动差值器  va.setDuration(500);  va.start(); } @Override public void stopAnim() {  backColor = getResources().getColor(R.color.color_8b90af);  mBackPaint.setColor(backColor); } /**改变控制点*/ public void setPull_height(int y){  pull_height = y;  invalidate(); }}
View Code

使用也是很简单的一两行代码

只需要添加一个头部和在回调中把拉伸的值传进去就可以了,到这里背景头部就好了,下面就是继续叠加头部效果了