最近写了个下拉控件,和几个下拉的头部样式,下拉控件可以连续添加叠加几个头部视图
下面是没有添加任何头部尾部的视图下拉效果
一步一步来介绍,先介绍这个下拉效果,在介绍自定义的头部
首先在使用上,和普通的控件没有两样,拿recyclerview来做例子,因为recyclerview使用比较多,而且可以替代很多的列表控件
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:app="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) { } }); }class="code_img_closed" src="/Upload/Images/2017120813/0015B68B3C38AA5B.gif" alt="">
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() {//布局加载成xml时触发 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 @Override 105 public boolean onTouch(View v, MotionEvent event) { 106 return onFingerTouch(event); 107 } 108 }); 109 } 110 } 111 112 /** 113 * 设置头部View 114 */ 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 * 设置尾部View 125 */ 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也大于headheight 178 //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.bottomMargin 184 // +" 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 @Override 195 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,获取第一个子控件的top 261 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,获取第一个子控件的top 296 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的topInset 320 */ 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 }logs_code_collapse">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也大于headheight 45 //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.bottomMargin 51 // +" 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 @Override 62 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 @Override 132 public View getView() { 133 return this; 134 } 135 136 @Override 137 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 @Override 144 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 @Override 158 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效果
下面看看最终的一个效果
这里我们的头部一共添加了三个,最底层是扇形的下拉头部,第二层是一个圆形的拉扯粘性效果,第三层是一排小圆点左右晃动的效果,三层叠加
使用上一样
而尾部基本没有什么逻辑代码,就是一个xml的展示
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