Android View 下拉刷新之头部效果自定义 [水]
来源:互联网 发布:红警3起义时刻兵种数据 编辑:程序博客网 时间:2024/05/02 00:27
尊重原创转载请注明:http://blog.csdn.net/bfbx5173/article/details/49814551
Rick 镇楼
好久没来博客耍耍了。俗话说学到的东西要多复习,不然就会忘记,今日无聊来一发。
首先呢,这个下拉刷新基于开源框架 android-Ultra-Pull-To-Refresh
在build.gradle中
compile 'in.srain.cube:ultra-ptr:1.0.11'
so - 尚不清楚android-Ultra-Pull-To-Refresh的朋友[ 戳我查看 android-Ultra-Pull-To-Refresh 源码解析]
这是 Ultra中提供的常规默认刷新的下拉头PtrClassicDefaultHeader。我们来看看源码
public class PtrClassicDefaultHeader extends FrameLayout implements PtrUIHandler { // 省略所有代码...}
没错,只不过是一个实现了接口 PtrUIHandler的View而已。那PtrUIHandler 定义了什么呢?
public interface PtrUIHandler { void onUIReset(PtrFrameLayout var1); void onUIRefreshPrepare(PtrFrameLayout var1); void onUIRefreshBegin(PtrFrameLayout var1); void onUIRefreshComplete(PtrFrameLayout var1); void onUIPositionChange(PtrFrameLayout var1, boolean var2, byte var3, PtrIndicator var4);}
看样子是定义了一些“时机”,在这些“时机”下我们的这个这个view可以做一些羞羞的事情。
管它的呢,不管怎么分析,总是要写的。既然是view,那么先走上。
感觉下需要的高度,蓝色的背景和白色的Loading对应的位置、大小。然后把这个画粗来。
定义 PtrMDHeader
public class PtrMDHeader extends View implements PtrUIHandler { private final Paint paint = new Paint(); private final RectF ovalLoading = new RectF(); private final RectF ovalBlueBack = new RectF(); private int mWidth; private int mHeight; private int centerX; private int centerY; private int mLoadingStrokeWidth; private int radiusBlue; private int radiusWhite; public PtrMDHeader(Context context) { this(context, null); } public PtrMDHeader(Context context, AttributeSet attrs) { super(context, attrs); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final float scale = getResources().getDisplayMetrics().density; // 先拿60dip的高度耍耍 int heightSize = MeasureSpec.makeMeasureSpec((int) (60 * scale + 0.5f), View.MeasureSpec.EXACTLY); super.onMeasure(widthMeasureSpec, heightSize); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); mWidth = w; // PtrMDHeader的宽度 mHeight = h; // PtrMDHeader的高度 centerX = mWidth / 2; // X轴中心点 centerY = mHeight / 2; // Y轴中心点 radiusBlue = mHeight / 4; // 蓝色背景半径 radiusWhite = radiusBlue / 2; // 白色Loading半径 mLoadingStrokeWidth = radiusBlue / 8; // 白色Loading尺寸 /* 白色Loading所在矩形区域 */ ovalLoading.left = centerX - radiusWhite; ovalLoading.right = centerX + radiusWhite; ovalLoading.top = centerY - radiusWhite; ovalLoading.bottom = centerY + radiusWhite; /** 蓝色扇形所在矩形区域*/ ovalBlueBack.left = centerX - radiusBlue; ovalBlueBack.right = centerX + radiusBlue; ovalBlueBack.top = centerY - radiusBlue; ovalBlueBack.bottom = centerY + radiusBlue; } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawColor(0xffcccccc); paint.setAntiAlias(true); // 蓝色背景 paint.setColor(0xFF5AB1E8); paint.setStyle(Paint.Style.FILL); canvas.drawCircle(centerX, centerY, radiusBlue, paint); // 白色loading paint.setColor(0xFFFFFFFF); paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(mLoadingStrokeWidth); canvas.drawArc(ovalLoading, 90, 270, false, paint); } @Override public void onUIReset(PtrFrameLayout ptrFrameLayout) { Log.d("PtrMDHeader", "onUIReset"); } @Override public void onUIRefreshPrepare(PtrFrameLayout ptrFrameLayout) { Log.d("PtrMDHeader", "onUIRefreshPrepare"); } @Override public void onUIRefreshBegin(PtrFrameLayout ptrFrameLayout) { Log.d("PtrMDHeader", "onUIRefreshBegin"); } @Override public void onUIRefreshComplete(PtrFrameLayout ptrFrameLayout) { Log.d("PtrMDHeader", "onUIRefreshComplete"); } @Override public void onUIPositionChange(PtrFrameLayout ptrFrameLayout, boolean b, byte b1, PtrIndicator ptrIndicator) { Log.d("PtrMDHeader", "onUIPositionChange b:" + b + ", b1:" + b1 + "percent:" + ptrIndicator.getCurrentPercent()); }}东西写完了,还需要一些配置,在xml中:
<in.srain.cube.views.ptr.PtrFrameLayout android:id="@+id/lay_ptr_frame" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:id="@+id/store_house_ptr_image_content" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#FFFFFF" android:gravity="center_horizontal"> <TextView android:id="@+id/tx_test" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" /> <ImageView android:contentDescription="@string/app_name" android:id="@+id/img_test" android:layout_width="200dip" android:layout_height="200dip" android:layout_gravity="bottom|center_horizontal" android:background="#ccc" /> </LinearLayout></in.srain.cube.views.ptr.PtrFrameLayout>
然后在activity中
store_house_ptr_image_content = findViewById(R.id.store_house_ptr_image_content);lay_ptr_frame = (PtrFrameLayout) findViewById(R.id.lay_ptr_frame);final PtrMDHeader header = new PtrMDHeader(getContext());lay_ptr_frame.setHeaderView(header);lay_ptr_frame.addPtrUIHandler(header);lay_ptr_frame.setPtrHandler(new PtrHandler() { @Override public boolean checkCanDoRefresh(PtrFrameLayout frame, View content, View header) { return PtrDefaultHandler.checkContentCanBePulledDown(frame, store_house_ptr_image_content, header); } @Override public void onRefreshBegin(final PtrFrameLayout frame) { // 模仿延时3000毫秒 postDelayed(new Runnable() { @Override public void run() { frame.refreshComplete(); } }, 3000); }});
好配菜都准备完成了,看下效果。
6666666,似乎少了很多东西。不过万事开头难,头开出来了后面就慢慢来呗。
继续分析:
少了很多状态,这个应该有很多状态,不同的状态下显示不一样。so
/** 状态-闲置 */ public static final byte STATE_IDLE = 0; /** 状态-下拉去刷新 */ public static final byte STATE_PULL_TO_REFRESH = 1; /** 状态-释放去刷新 */ public static final byte STATE_RELEASE_TO_REFRESH = 2; /** 状态-刷新中 */ public static final byte STATE_REFRESHING = 3; /** 状态-刷新完成 */ public static final byte STATE_COMPLETED = 4; /** 当前状态 */ private byte mState = STATE_IDLE;
应该是这几个状态没跑了。
细心的朋友可以发现在之前的代码中,偷偷打了写log。
当我们下拉的时候onUIPositionChange(PtrFrameLayout ptrFrameLayout, boolean b, byte b1, PtrIndicator ptrIndicator)就会一直走,而ptrIndicator中携带了位置等一些信息。
在这里我们可以把手指拖拽的2个状态[下拉去刷新-释放去刷新]补上
private float currPercent;@Overridepublic void onUIPositionChange(PtrFrameLayout ptrFrameLayout, boolean b, byte b1, PtrIndicator ptrIndicator) { Log.d("PtrMDHeader", "onUIPositionChange b:" + b + ", b1:" + b1 + "percent:" + ptrIndicator.getCurrentPercent()); currPercent = ptrIndicator.getCurrentPercent(); // 下拉刷新?释放刷新?下拉刷新?释放刷新?下拉.. mState = currPercent < 1f ? STATE_PULL_TO_REFRESH : STATE_RELEASE_TO_REFRESH; invalidate();}这里currPercent的值会随手指拖拽的变化而变化,PtrMDHeader 的状态也根据currPercent的变化而变化。
然后就invalidate()啦,那么代码转移到onDraw中:
@Overrideprotected void onDraw(Canvas canvas) { super.onDraw(canvas); // 临时加上背景灰色利于分析 canvas.drawColor(0xffcccccc); paint.setAntiAlias(true); switch (mState){ case STATE_PULL_TO_REFRESH: // 刚刚下拉中,还木有到释放刷新的程度。执行这个case default: break; case STATE_RELEASE_TO_REFRESH: // 下拉的距离够了,手指可以松开释放刷新了。不松就执行这个case // 蓝色背景 paint.setColor(0xFF5AB1E8); paint.setStyle(Paint.Style.FILL); canvas.drawCircle(centerX, centerY, radiusBlue, paint); // 白色loading paint.setColor(0xFFFFFFFF); paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(mLoadingStrokeWidth); canvas.drawArc(ovalLoading, 90, 270, false, paint); break; }}
来,哥图多
这里可以看出来,在STATE_PULL_TO_REFRESH中也做了2段动画。第一段是蓝色背景以一个扇形慢慢补全。第二段是白色Loading的补全。
/** 初始 */ private static final float STEP_IDLE = 0f; /** 第一段和第二段分割界线 */ private static final float STEP_HALF_OF_PULL = 0.7f; /** 下拉刷新和释放刷新分割界线 */ private static final float STEP_RELEASE_TO_REFRESH = 1.0f;
case STATE_PULL_TO_REFRESH:default: if (currPercent < STEP_HALF_OF_PULL) { // 第一段 背景以扇形的方式扇形出来 final float p1 = (currPercent - STEP_IDLE) / (STEP_HALF_OF_PULL - STEP_IDLE); paint.setColor(Color.argb((int) (255 * p1), 90, 177, 232)); paint.setStyle(Paint.Style.FILL); canvas.drawArc(ovalBlueBack, -90, 360 * p1, true, paint); } else { // 第一段 白色loading以旋转的方式出来 final float p2 = (currPercent - STEP_HALF_OF_PULL) / (STEP_RELEASE_TO_REFRESH - STEP_HALF_OF_PULL); paint.setColor(0xFF5AB1E8); paint.setStyle(Paint.Style.FILL); canvas.drawCircle(centerX, centerY, radiusBlue, paint); paint.setColor(Color.argb((int) (255 * p2), 255, 255, 255)); paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(mLoadingStrokeWidth); canvas.drawArc(ovalLoading, -90 + 180 * p2, 270, false, paint); }break;
第一段扇形的效果看不见加个偏移。
还记得这个效果吗~ 在 SwitchView中用过哟
case STATE_PULL_TO_REFRESH:default: final float p = (currPercent - STEP_IDLE) / (STEP_RELEASE_TO_REFRESH - STEP_IDLE); canvas.save(); canvas.translate(0, (radiusBlue * 1.8f) * (1 - p)); // ... 省略刚才的代码 canvas.restore();break;
有没有觉得左边[做了偏移]效果比右边[没有偏移]好呀
好我们继续研究刷新
把刷新拆解成这4个状态。红色表示起点,蓝色表示重点。用画圆弧的api[drawArc]来做。
比如状态[1]到状态[2],起点经过了360度,而圆弧长度由270度下降到30度。依次分析那么:
private float mAnimPercent; // 动画进度,控制刷新中的显示private ValueAnimator valueAnimator;private final AccelerateDecelerateInterpolator adi = new AccelerateDecelerateInterpolator();private void doRefreshingAnim() { if (valueAnimator != null) { valueAnimator.cancel(); valueAnimator = null; } valueAnimator = ValueAnimator.ofFloat(0f, 1f, 2f, 3f,4f); valueAnimator.setRepeatMode(ValueAnimator.RESTART); valueAnimator.setRepeatCount(ValueAnimator.INFINITE); // 无限重复 valueAnimator.setDuration(2000); valueAnimator.setInterpolator(adi); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mAnimPercent = (Float) animation.getAnimatedValue(); invalidate(); } }); mState = STATE_REFRESHING; valueAnimator.start();}switch (mState) { case STATE_REFRESHING: // 蓝色背景 paint.setColor(0xFF5AB1E8); paint.setStyle(Paint.Style.FILL); canvas.drawCircle(centerX, centerY, radiusBlue, paint); // 白色loading paint.setColor(0xFFFFFFFF); paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(mLoadingStrokeWidth); final int s = (int) mAnimPercent; final float mp; switch (s) { case 0: mp = mAnimPercent; canvas.drawArc(ovalLoading, 90 + (360 * mp), 30 + 240 * (1 - mp), false, paint); break; case 1: mp = mAnimPercent - 1; canvas.drawArc(ovalLoading, 90 + (180 * mp), 30 + 240 * mp, false, paint); break; case 2: mp = mAnimPercent - 2; canvas.drawArc(ovalLoading, -90 + (360 * mp), 30 + 240 * (1 - mp), false, paint); break; case 3: mp = mAnimPercent - 3; canvas.drawArc(ovalLoading, -90 + (180 * mp), 30 + 240 * mp, false, paint); break; } break;}
没错,case 0,1,2,3 分别表示图中4个步骤。
@Overridepublic void onUIPositionChange(PtrFrameLayout ptrFrameLayout, boolean b, byte b1, PtrIndicator ptrIndicator) { Log.d("PtrMDHeader", "onUIPositionChange b:" + b + ", b1:" + b1 + "percent:" + ptrIndicator.getCurrentPercent()); currPercent = ptrIndicator.getCurrentPercent(); if (mState != STATE_REFRESHING) { // 刷新的状态。invalidate的逻辑由valueAnimator来控制。 mState = currPercent < 1f ? STATE_PULL_TO_REFRESH : STATE_RELEASE_TO_REFRESH; invalidate(); }}@Overridepublic void onUIRefreshBegin(PtrFrameLayout ptrFrameLayout) { Log.d("PtrMDHeader", "onUIRefreshBegin"); doRefreshingAnim();}最后在onUIRefreshBegin启动这段动画
当然这里想怎么搞就怎么搞~ 画布在手天下我有。
最后当刷新完成的时候
// 刷新完成时,刷新成功或失败的bmpprivate Bitmap bmpSuccess, bmpFail;@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); BitmapFactory.Options opt = new BitmapFactory.Options(); opt.outWidth = radiusBlue; opt.outHeight = radiusBlue; bmpSuccess = BitmapFactory.decodeResource(getResources(), R.mipmap.ptr_header_md_success, opt); bmpFail = BitmapFactory.decodeResource(getResources(), R.mipmap.ptr_header_md_fail, opt);}// 根据刷新结果显示不同的内容private Bitmap getRefreshResultBitmap() { return refreshResult ? bmpSuccess : bmpFail;}private final OvershootInterpolator oi = new OvershootInterpolator();private void doCompleteAnim() { if (valueAnimator != null) { valueAnimator.cancel(); valueAnimator = null; } valueAnimator = ValueAnimator.ofFloat(0f, 1f); valueAnimator.setDuration(400); valueAnimator.setInterpolator(oi); valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { mAnimPercent = (Float) animation.getAnimatedValue(); invalidate(); } }); mState = STATE_COMPLETED; valueAnimator.start();}switch (mState) { case STATE_COMPLETED: paint.setColor(0xFF5AB1E8); paint.setStyle(Paint.Style.FILL); canvas.drawCircle(centerX, centerY, radiusBlue, paint); canvas.save(); canvas.scale(mAnimPercent, mAnimPercent, centerX, centerY); final Bitmap bmpResult = getRefreshResultBitmap(); canvas.drawBitmap(bmpResult, centerX - bmpResult.getWidth() / 2, centerY - bmpResult.getHeight() / 2, paint); canvas.restore(); break;}/** 提供一个公开的接口来调用 refreshResult表示刷新结果 */public void refreshComplete(boolean refreshResult, final PtrFrameLayout frame) { this.refreshResult = refreshResult; doCompleteAnim(); postDelayed(new Runnable() { @Override public void run() { frame.refreshComplete(); } }, 600);}
最后在activity中调用
lay_ptr_frame.setPtrHandler(new PtrHandler() { @Override public boolean checkCanDoRefresh(PtrFrameLayout frame, View content, View header) { return PtrDefaultHandler.checkContentCanBePulledDown(frame, store_house_ptr_image_content, header); } @Override public void onRefreshBegin(final PtrFrameLayout frame) { postDelayed(new Runnable() { @Override public void run() { // 模仿延时3000毫秒 boolean refreshResult = true; header.refreshComplete(refreshResult, frame); } }, 3000); }});分别给refreshResult传入true,false
当然这2个图片可以替换成随意内容。
貌似刷新完成后,header收起的时候显示不对~
@Overridepublic void onUIPositionChange(PtrFrameLayout ptrFrameLayout, boolean b, byte b1, PtrIndicator ptrIndicator) { currPercent = ptrIndicator.getCurrentPercent(); if (mState != STATE_REFRESHING && mState != STATE_COMPLETED) { // 状态为刷新中或者刷新完成的时候,不根据currPercent来更新UI。 mState = currPercent < STEP_RELEASE_TO_REFRESH ? STATE_PULL_TO_REFRESH : STATE_RELEASE_TO_REFRESH; invalidate(); }}
最后刷新完成重置一下状态
@Overridepublic void onUIReset(PtrFrameLayout ptrFrameLayout) { Log.d("PtrMDHeader", "onUIReset"); mState = STATE_IDLE;}
好了,到此大功告成! 一样才300行代码。
[逼格提升入口]戳我获得完整PtrMDHeader源码[附含详细使用教程]
fork源码记得点赞Star哟,仔细阅读README.md文件。你会发现使用起来so easy。
~
- Android View 下拉刷新之头部效果自定义 [水]
- 自定义View之粘性下拉刷新效果
- Android 自定义View UC下拉刷新效果(一)
- Android 自定义View UC下拉刷新效果(二)
- android 下拉刷新可自定义刷新头部和底部
- 下拉刷新头部视差效果
- Android自定义View之(下拉刷新+侧滑删除)
- 自定义头部的下拉刷新
- Andriod自定义View之(下拉刷新)
- Android自定义View-下拉刷新控件
- Android自定义View实现下拉刷新控件
- android 自定义下拉刷新动画效果
- 自定义View刷新头部,已适配AbsListView、RecyclerView
- Android自定义View之快速实现下拉刷新, 点击加载更多ListView
- Android自定义控件之仿美团下拉刷新
- Android自定义控件之仿美团下拉刷新
- Android自定义控件之仿美团下拉刷新
- 自定义View之实现ListView的下拉刷新
- SQLServer基本函数
- Python 获取最长单词的两种方法
- Xshell远程连接Linux服务器出错——Could not connect to '114.214.166.5' (port 22): Connection failed.
- ViewPager禁止左右滑动
- Dex 文件格式详解
- Android View 下拉刷新之头部效果自定义 [水]
- spring Scheduled cron定时调度时间的设置
- Android之计算缓存大小并且清空缓存
- Fresco正传(4):DraweeController分析
- 前端笔记
- 七天学会NodeJS
- linux下命令行装oracle
- java Swing 心得体会
- 文章标题