最近写了个下拉控件,和几个下拉的头部样式,下拉控件可以连续添加叠加几个头部视图
下面是没有添加任何头部尾部的视图下拉效果
一步一步来介绍,先介绍这个下拉效果,在介绍自定义的头部
首先在使用上,和普通的控件没有两样,拿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/2017120719/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 这个下拉控件里面的代码是完整的,没有去截取,不过写了非常非常详细的注释,然后控件里面的添加头部视图是通过接口来控制添加的,外加对外提供的状态回调,建造者模式返回本身对象,这样方便扩展
然后现在开始定义头部效果,说到头部效果,扇形的下拉回弹首选了,因为已经有很多例子了,所以我也模仿做了一个,还有一个原因是为了后面的头部做铺垫,叠加头部,那么最先添加的头部肯定要有当背景的觉悟了,先看效果图
上代码,同样是非常非常详细的注释
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
使用也是很简单的一两行代码
只需要添加一个头部和在回调中把拉伸的值传进去就可以了,到这里背景头部就好了,下面就是继续叠加头部效果了