你的位置:首页 > 软件开发 > 操作系统 > 自定义可扩展叠加头部的下拉控件

自定义可扩展叠加头部的下拉控件

发布时间:2017-12-07 20:00:20
最近写了个下拉控件,和几个下拉的头部样式,下拉控件可以连续添加叠加几个头部视图下面是没有添加任何头部尾部的视图下拉效果 一步一步来介绍,先介绍这个下拉效果,在介绍自定义的头部首先在使用上,和普通的控件没有两样,拿recyclerview来做例子,因为recyclerview使 ...

自定义可扩展叠加头部的下拉控件

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

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

自定义可扩展叠加头部的下拉控件自定义可扩展叠加头部的下拉控件

 

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

首先在使用上,和普通的控件没有两样,拿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 这个下拉控件里面的代码是完整的,没有去截取,不过写了非常非常详细的注释,但是还是大致的讲下实现的流程

 

首先是继承一个布局,方面添加各种节点,比较直接view的话很难实现,代码量也多,伤不起,也没时间

实现构造函数后初始化各种操作,这里给头部的下拉提供一个容器,因为需要叠加添加头部,所以需要一个父容器去控制子头部的一个添加和删除的控制,有了这个布局,那么就可以控制这个容器去做一些下拉的效果,还有往里面添加一些头部效果了

自定义可扩展叠加头部的下拉控件

初始化后我们需要获取recyclerview控件了,因为需要监听它的一些操作和状态,这里不让它传进去,因为这样依赖性太强了,不传进来也可以很好的获取,比较是子view视图,可以直接通过getChildAt获取

自定义可扩展叠加头部的下拉控件

获取到recyclerview那么可以监听它的一个touch事件,而这里的list是一个view类型,至于为什么不是recycyclerview类型我想就不用多说了,如果直接是recyclerview类型的话那我跟直接传参数进来就没什么两样了,这样的做法就没意义了

重点就是这个监听事件了,想想也知道,下拉上拉所有的效果都需要去监听它的一个滑动状态来判断的,所以核心的操作基本在这里面了

 1 public boolean onFingerTouch(MotionEvent ev) { 2   isTop = isViewToTop(list,mTouchSlop); 3   isBottom = isViewToBottom(list,mTouchSlop); 4 //  Log.e(TAG,"isTop "+isTop+" isBottom "+isBottom); 5   switch (ev.getAction()) { 6    case MotionEvent.ACTION_DOWN : 7     currentState = LOADING; 8     downY = (int) ev.getY(); 9     break;10    case MotionEvent.ACTION_MOVE :11     if (!isTop && !isBottom)//没有到顶,无需计算操作12      break;13     int moveY = (int) ev.getY();14     int diff = (int) (((float)moveY - (float)downY) / RATIO);15 //    int paddingTop = -mHeadLayout.getHeight() + diff;16     int paddingTop = diff;17     if (paddingTop>0 && isTop) {18      //向下滑动多少后开始启动刷新,Margin判断是为了限制快速用力滑动的时候导致头部侵入的高度不够就开始加载了19      if (paddingTop >= mHeadHeight && (listParam.topMargin >= mHeadHeight) && currentState == DOWN_REFRESH) { // 完全显示了.20 //      Log.i(TAG, "松开刷新 RELEASE_REFRESH");21       currentState = RELEASE_REFRESH;22       refreshHeaderView();23       start();24      } else if (currentState == LOADING) { // 没有显示完全25 //      Log.i(TAG, "下拉刷新 DOWN_PULL_REFRESH");26       currentState = DOWN_REFRESH;27       refreshHeaderView();28      }29      if (paddingTop <= (mHeadHeight+10) && !isStart) {//已经处于运行刷新状态的时候禁止设置30       listParam.setMargins(0, paddingTop, 0, 0);31       list.setLayoutParams(listParam);32       if (callBack != null)33        callBack.pullListener(paddingTop);34      }35 36     }else if (isBottom){37      //限制上滑时不能超过底部的宽度,不然会超出边界38      //mHeadHeight+20 上滑设置的margin要超过headheight,不然下面判断的大于headheight不成立,下面的margin基础上面设置后的参数39      if (Math.abs(paddingTop) <= (mHeadHeight+10) && !isStart) {//已经处于运行刷新状态的时候禁止设置40       listParam.setMargins(0, 0, 0, -paddingTop);41       footParam.setMargins(0,0,0,-paddingTop-mHeadHeight);42       list.setLayoutParams(listParam);43      }44      //如果滑动的距离大于头部或者底部的高度,并且设置的margin也大于headheight45      //listParam用来限制recyclerview列表迅速滑动,footParam用来限制bottom foothead迅速滑动导致没有达到head的高度就开始加载了46      if (Math.abs(paddingTop) >= mHeadHeight && (listParam.bottomMargin >= mHeadHeight || footParam.bottomMargin >= 0))47       isLoadingMore = true;//头部是否拉取到位,然后执行加载动画48 49     }50 //    Log.e(TAG,"paddingTop "+paddingTop +" mHeadHeight "+mHeadHeight+ " topMargin "+listParam.topMargin+" bottomMargin "+listParam.bottomMargin51 //      +" footParam bottom "+footParam.bottomMargin);52 //    Log.i(TAG,"paddingTop "+paddingTop);53     break;54    case MotionEvent.ACTION_UP :55     currentState = LOADING;56     refreshHeaderView();57     if (isLoadingMore){58      isLoadingMore = false;59      isStart = true;//是否开始加载60      postDelayed(new Runnable() {61       @Override62       public void run() {63 //       Log.i(TAG, "停止 END");64 //       currentState = END;65        refreshHeaderView();66        listParam.setMargins(0, 0, 0, 0);67        footParam.setMargins(0,0,0,-mHeadHeight);68        list.setLayoutParams(listParam);69        stop();70       }71      },2000);72     }else{73      if (!isStart){74       // 隐藏头布局75       listParam.setMargins(0, 0,0,0);76       footParam.setMargins(0,0,0,-mHeadHeight);77       list.setLayoutParams(listParam);78      }79     }80 //    Log.i(TAG, "松开 REFRESHING");81     break;82    default :83     break;84   }85   return super.onTouchEvent(ev);86  }

注释没得说,大致的一个流程就是分下拉和下拉的判断

1:按下后获取getY,就是y轴坐标,也就是起点,后面用来计算滑动的距离

2:手指滑动,同样在获取getY,然后相减,得到滑动的距离Y,然后上面recyclerview对象的作用来了,这里的上滑还是下滑不采用手指滑动计算来判断,而是用recyclerview来判断是否到顶,如果到top顶点,那么就开始下拉,反之就是上拉操作,毕竟手势的判断不是很精确,这样核心的判断出来,一个上,一个下,至于具体的逻辑看着代码根据注释看了,因为那部分的注释特别详细,而值得一说的就是这里的下拉上拉是采用设置margin来做的

自定义可扩展叠加头部的下拉控件

3:手指滑动完毕,抬起操作,到这里基本就是收尾的工作了,这里因为是写死的数据,没有网络访问,所以就是定死,设置拉下时长两秒,然后完成下拉操作后就还原起点

上拉过程中提供对外的监听回调,这里有两个回调,第一个回调是带有状态和状态值的一个参数,第二个回调就是拉下的一个坐标距离,在后面的头部视图会用到,这个可以自行扩展

 自定义可扩展叠加头部的下拉控件

然后就是头部的添加操作,之前说的容器用来放下拉视图,而要叠加效果添加多个头部就需要一个集合存放了,用来控制每个头部

自定义可扩展叠加头部的下拉控件

接下来就是addview的操作了

自定义可扩展叠加头部的下拉控件

因为每个头部都需要继承head接口,这是为了减低耦合度,不直接依赖头部,可以自行扩展,建造者模式返回本身对象,这样方便扩展

 这里提供了三个控制方法,getView,这个必须的,可以获取到每个头部的实例,然后操作,然后每个头部可以添加动画,所以有开始动画和关闭动画的方法

public interface IHeadView { View getView(); void startAnim(); void stopAnim();}

这样在每个头部如果有动画的话就能在回调接口里实现了

自定义可扩展叠加头部的下拉控件

而启动和停止动画肯定是跟随着下拉的状态变化而启动的,所以调用都在touch监听里面,至于什么时候调用可以根据具体需求去调用

 

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

自定义可扩展叠加头部的下拉控件

 

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

自定义可扩展叠加头部的下拉控件自定义可扩展叠加头部的下拉控件
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

扇形下拉头部实现流程也比较简单

首先抛开动画,是先绘制出一个矩形,然后下面的扇形利用二级的杯赛二线绘制,以y抽为控制点,达到拉伸变长的一个效果

自定义可扩展叠加头部的下拉控件

 

然后拉到最低的时候在开启一个回弹动画或者甩动的动画

 自定义可扩展叠加头部的下拉控件

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

自定义可扩展叠加头部的下拉控件

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

 自定义可扩展叠加头部的下拉控件

看起来好像是一个视图,其实是两个,第二个头部的效果也是比较简单的一个效果,绘制了一个粘性的回弹效果

自定义可扩展叠加头部的下拉控件自定义可扩展叠加头部的下拉控件
 1 package com.fragmentapp.view.refresh; 2  3 import android.animation.ValueAnimator; 4 import android.content.Context; 5 import android.graphics.Canvas; 6 import android.graphics.Color; 7 import android.graphics.Paint; 8 import android.graphics.Path; 9 import android.graphics.PointF; 10 import android.support.annotation.Nullable; 11 import android.util.AttributeSet; 12 import android.util.Log; 13 import android.view.View; 14 import android.view.animation.OvershootInterpolator; 15  16 import com.fragmentapp.R; 17  18 /** 19  * Created by liuzhen on 2017/12/4. 20 */ 21  22 public class StickyHeadView extends View implements IHeadView { 23  24  private Paint mPaint; 25  private int bg; 26  private float mStaicRadius = 20f;// 直径 27  private float mMoveRadius = 20f;// 直径 28  29  // 存储静态的两个点 30  private PointF[] mStaticPointFs = new PointF[2]; 31  // 存储移动的两个点 32  private PointF[] mMovewPointFs = new PointF[2]; 33  // 控制点 34  private PointF mControlPointF = new PointF(); 35  // 静态点 36  private PointF staticCenterPointF = null; 37  // 移动点 38  private PointF movewCenterPointF = null; 39  40  public StickyHeadView(Context context) { 41   this(context, null, 0); 42  } 43  44  public StickyHeadView(Context context, @Nullable AttributeSet attrs) { 45   this(context, attrs, 0); 46  } 47  48  public StickyHeadView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 49   super(context, attrs, defStyleAttr); 50   init(); 51  } 52  53  private void init() { 54   mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 55   bg = getResources().getColor(R.color.color_8babaf); 56   mPaint.setColor(bg); 57  } 58  59  @Override 60  protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 61   super.onLayout(changed, left, top, right, bottom); 62   if (changed) { 63    staticCenterPointF = new PointF(getWidth() / 2, getHeight()/4); 64    movewCenterPointF = new PointF(getWidth() / 2, getHeight()/4); 65   } 66  } 67  68  @Override 69  protected void onDraw(Canvas canvas) { 70   // 1、获得偏移量 71   float yOffset = staticCenterPointF.y - movewCenterPointF.y; 72   float xOffset = staticCenterPointF.x - movewCenterPointF.x; 73   // 2、有了偏移量就可以求出两点斜率了 74   Double lineK = 0.0; 75   if (xOffset != 0f) { 76    lineK = (double) (yOffset / xOffset); 77   } 78   // 3、通过工具求得两个点的集合 79   mMovewPointFs = getIntersectionPoints(movewCenterPointF, mMoveRadius, lineK); 80   mStaticPointFs = getIntersectionPoints(staticCenterPointF, mStaicRadius, lineK); 81   // 4、通过公式求得控制点 82   mControlPointF = getMiddlePoint(staticCenterPointF, movewCenterPointF); 83  84   // 保存画布状态,保存方法之后的代码,能够调用Canvas的平移、放缩、旋转、裁剪等操作 85 //  canvas.save(); 86  87   // 工型绘制 88   Path path = new Path(); 89   // 左上角点 90   path.moveTo(mStaticPointFs[0].x, mStaticPointFs[0].y); 91   // 上一边的弯度和右上角点 92   path.quadTo(mControlPointF.x, mControlPointF.y, mMovewPointFs[0].x, mMovewPointFs[0].y); 93   // 右下角点 94   path.lineTo(mMovewPointFs[1].x, mMovewPointFs[1].y); 95   // 下一边的弯度和左下角点 96   path.quadTo(mControlPointF.x, mControlPointF.y, mStaticPointFs[1].x, mStaticPointFs[1].y); 97   // 关闭后,会回到最开始的地方,形成封闭的图形 98   path.close(); 99 100   canvas.drawPath(path, mPaint);101 102   canvas.drawCircle(staticCenterPointF.x, staticCenterPointF.y, mStaicRadius, mPaint);103 //  // 画移动的大圆104   canvas.drawCircle(movewCenterPointF.x, movewCenterPointF.y, mMoveRadius, mPaint);105 106   // 恢复上次的保存状态107 //  canvas.restore();108 109  }110  /**拉扯移动点*/111  public void move(float downY) {112   //以中间点为定点113   updateMoveCenter(getWidth() / 2 + downY, movewCenterPointF.y);114   updateStaticCenter(getWidth() / 2 - downY, staticCenterPointF.y);115  }116 117 118  /**119   * 更新移动的点120  */121  private void updateMoveCenter(float downX, float downY) {122   movewCenterPointF.set(downX, downY);123   invalidate();124  }125 126  private void updateStaticCenter(float downX, float downY) {127   staticCenterPointF.set(downX, downY);128   invalidate();129  }130 131  @Override132  public View getView() {133   return this;134  }135 136  @Override137  public void startAnim() {138   bg = getResources().getColor(R.color.color_8b90af);139   mPaint.setColor(bg);140   //甩动动画141   ValueAnimator vAnim = ValueAnimator.ofFloat(-30,0,50,0);142   vAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {143    @Override144    public void onAnimationUpdate(ValueAnimator animation) {145 //    float percent = animation.getAnimatedFraction();146     float val = (float)animation.getAnimatedValue();147 //    Log.e("tag","percent "+percent + " percent*val "+percent*val);148     updateMoveCenter(getWidth() / 2 + val, movewCenterPointF.y);149     updateStaticCenter(getWidth() / 2 - val, staticCenterPointF.y);150    }151   });152   vAnim.setInterpolator(new OvershootInterpolator(2));//甩动差值器153   vAnim.setDuration(1000);154   vAnim.start();155  }156 157  @Override158  public void stopAnim() {159   bg = getResources().getColor(R.color.color_8babaf);160   mPaint.setColor(bg);161  }162 163  /**164   * Get the point of intersection between circle and line. 获取165   * 通过指定圆心,斜率为lineK的直线与圆的交点。166  */167  private PointF[] getIntersectionPoints(PointF pMiddle, float radius, Double lineK) {168   PointF[] points = new PointF[2];169 170   float radian, xOffset = 0, yOffset = 0;171   if (lineK != null) {172    radian = (float) Math.atan(lineK);173    xOffset = (float) (Math.sin(radian) * radius);174    yOffset = (float) (Math.cos(radian) * radius);175   } else {176    xOffset = radius;177    yOffset = 0;178   }179   points[0] = new PointF(pMiddle.x + xOffset, pMiddle.y - yOffset);180   points[1] = new PointF(pMiddle.x - xOffset, pMiddle.y + yOffset);181 182   return points;183  }184 185 186  /**187   * Get middle point between p1 and p2. 获得两点连线的中点188  */189  private PointF getMiddlePoint(PointF p1, PointF p2) {190   return new PointF((p1.x + p2.x) / 2.0f, (p1.y + p2.y) / 2.0f);191  }192 193 }
View Code

自定义可扩展叠加头部的下拉控件

而粘性效果会随着拉伸而左右拉扯,这里就是利用我们之前的回调接口去控制了,在头部写控制方法,然后在回调接口里把坐标传进去,就达到拉伸拉扯的效果了

自定义可扩展叠加头部的下拉控件

 粘性的动画其实是两个圆形,加上一个工型的贝塞尔线,然后也是通过回调获取拉伸的距离坐标,通过坐标,来达到控制两个圆的拉扯,下拉到底的时候在启动甩动动画,因为加入了贝塞尔的缘故,所以看起来就像是两个球有引力一般相互拉扯

工型贝塞尔的绘制也有详细的注释,工的两头都是一样的长度,所以圆也是一样的大小,拉伸的时候改变两个的圆的距离

自定义可扩展叠加头部的下拉控件

到这里核心的功能就介绍完了,接下来还有一些收尾的操作,我们的尾部,然后在定义一个我们的头部叠加进去,当然,尾部如果有需要也能做成叠加的一个容器,上面也的确是这么做的,不过一般的尾部都是比较简单的,所以没有做过多的一个样式,这里只是做了一个简单的progress效果

下面看看最终的一个效果

自定义可扩展叠加头部的下拉控件

 

 这里我们的头部一共添加了三个,最底层是扇形的下拉头部,第二层是一个圆形的拉扯粘性效果,第三层是一排小圆点左右晃动的效果,三层叠加

使用上一样

自定义可扩展叠加头部的下拉控件

而尾部基本没有什么逻辑代码,就是一个自定义可扩展叠加头部的下拉控件自定义可扩展叠加头部的下拉控件

package com.fragmentapp.view.refresh;import android.content.Context;import android.util.AttributeSet;import android.view.View;import android.widget.ProgressBar;/** * Created by liuzhen on 2017/11/24. */public class DefFootView extends ProgressBar implements IFootView{ public DefFootView(Context context) {  this(context,null,android.R.attr.progressBarStyleSmallInverse); } public DefFootView(Context context, AttributeSet attrs) {  this(context, attrs,0); } public DefFootView(Context context, AttributeSet attrs, int defStyleAttr) {  super(context, attrs, defStyleAttr);  init(); } private void init(){ } @Override public View getView() {  return this; } @Override public void startAnim() { } @Override public void stopAnim() { }}
View Code

本人在github上也上传了demo,可以提供下载,如果能让你有点感悟,请 star

GitHub:https://github.com/1024477951/FragmentApp

 

海外公司注册、海外银行开户、跨境平台代入驻、VAT、EPR等知识和在线办理:https://www.xlkjsw.com

原标题:自定义可扩展叠加头部的下拉控件

关键词:

*特别声明:以上内容来自于网络收集,著作权属原作者所有,如有侵权,请联系我们: admin#shaoqun.com (#换成@)。

可能感兴趣文章

我的浏览记录