Android仿苹果版QQ下拉刷新实现(一) ——打造简单平滑的通用下拉刷新控件
2017-08-11 18:45 海豚湾 阅读(630) 评论(0) 编辑 收藏 举报前言:
忙完了结婚乐APP的开发,终于可以花一定的时间放在博客上了.好了,废话不多说,今天我们要带来的效果是苹果版本的QQ下拉刷新.首先看一下目标效果以及demo效果:
因为此效果实现的步骤较多,所以今天博主要实现以上效果的第一步——打造一个通用的下拉刷新控件,具体效果如下:
GIF图片比较大,还希望读者能耐心等待一下下从效果图中可以看出,我们的下拉刷新的滑动还是很流畅的,可能大多数开发者用的是XListview或者PullToRefresh控件,在此博主本着能造轮子就造轮子的原则,打算自己打造一个自己喜欢的通用下拉刷新控件;下面就由博主来说明一下此控件是如何完成的:
一、自定义LinearLayout,手动加入下拉刷新布局
首先我们得准备好我们的刷新头部的布局,布局稍微复杂一点,分两层.一个是正在刷新时候的布局,一个是刷新完成的布局,在这里我用的RelativeLayout来布局,当然啦,除了RelativeLayout之外,FrameLayout和Linearlayout都可以直接或者间接的实现布局,下面上布局文件代码:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="60dp"> <LinearLayout android:id="@+id/ll_ok" android:layout_width="match_parent" android:layout_height="60dp" android:gravity="center" android:orientation="horizontal" android:visibility="gone"> <ImageView android:id="@+id/iv_ok" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@mipmap/pull_ok" /> <TextView android:id="@+id/tv_ok" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_marginLeft="10dp" android:gravity="center" android:text="刷新成功" android:textSize="14sp" android:textAppearance="?android:attr/textAppearance" android:textColor="#999999" android:textStyle="bold" /> </LinearLayout> <LinearLayout android:id="@+id/ll_refresh" android:layout_width="match_parent" android:layout_height="60dp" android:gravity="center"> <ProgressBar android:id="@+id/pb_refresh" style="?android:attr/progressBarStyleSmall" android:layout_width="15dp" android:layout_height="15dp" android:layout_centerVertical="true" android:layout_gravity="center" android:indeterminate="true" android:indeterminateDrawable="@drawable/pulling" android:visibility="gone" /> <ImageView android:id="@+id/iv_refresh" android:layout_width="15dp" android:layout_height="15dp" android:layout_centerVertical="true" android:src="@mipmap/pull_down" android:layout_toRightOf="@+id/pb_refresh" android:visibility="visible"/> <LinearLayout android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_toRightOf="@+id/iv_refresh" android:gravity="center" android:layout_marginLeft="10dp" android:orientation="vertical"> <TextView android:id="@+id/tv_tip" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:text="下拉刷新" android:singleLine="true" android:textColor="#9D9D9B" android:textSize="14sp" android:textStyle="bold" /> <TextView android:id="@+id/tv_time" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:layout_marginTop="5dp" android:text="上次刷新:" android:singleLine="true" android:textColor="#AEAEAC" android:textSize="12sp" /> </LinearLayout> </LinearLayout> </RelativeLayout>
高度我们限制死60dp的高度,这个很重要,因为我们的刷新控件都是基于这个高度来计算的;
接下来,我们开始编写自定义View,首先我们继承LinearLayout然后在初始化中加入下拉刷新布局,并定义好一些重要参数:
/** * 下拉刷新状态 */ public static final int REFRESH_BY_PULLDOWN=0; /** * 松开刷新状态 */ public static final int REFRESH_BY_RELEASE=1; /** * 正在刷新状态 */ public static final int REFRESHING=2; /** * 刷新成功状态 */ public static final int REFRESHING_SUCCESS=3; /** * 刷新失败状态 */ public static final int REFRESHING_FAILD=4; private View refreshView; private int refreshTargetTop; ObjectAnimator anim; //下拉刷新相关布局 LinearLayout ll_ok; RelativeLayout ll_refresh; ImageView iv_refresh, iv_ok; TextView tv_tip, tv_time, tv_ok; ProgressBar pb_refresh; private RefreshListener refreshListener; private int lastY; // 是否可刷新标记 private boolean isRefreshEnabled = true; /** * 刷新时间 */ Calendar LastRefreshTime; int refreshState=REFRESH_BY_PULLDOWN; private Context mContext; public YPXRefreshView(Context context) { this(context,null); } public YPXRefreshView(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; init(); } private void init() { LastRefreshTime = Calendar.getInstance(); //刷新视图顶端的的view refreshView = LayoutInflater.from(mContext).inflate(R.layout.layout_refresh_header, null); initRefreshView(); refreshTargetTop =-ScreenUtils.dpToPx(getResources(),60); LayoutParams lp = new LayoutParams(android.view.ViewGroup.LayoutParams.MATCH_PARENT, -refreshTargetTop); lp.topMargin = refreshTargetTop; lp.gravity = Gravity.CENTER; addView(refreshView, lp); anim = ObjectAnimator.ofFloat(refreshView, "ypx", 0.0f, 1.0f); } private void initRefreshView() { ll_ok = (LinearLayout) refreshView.findViewById(R.id.ll_ok); ll_refresh = (RelativeLayout) refreshView.findViewById(R.id.ll_refresh); iv_refresh = (ImageView) refreshView.findViewById(R.id.iv_refresh); iv_ok = (ImageView) refreshView.findViewById(R.id.iv_ok); tv_tip = (TextView) refreshView.findViewById(R.id.tv_tip); tv_time = (TextView) refreshView.findViewById(R.id.tv_time); tv_ok = (TextView) refreshView.findViewById(R.id.tv_ok); pb_refresh = (ProgressBar) refreshView.findViewById(R.id.pb_refresh); }
变量注释很详细,首先我们定义好下拉刷新的五种状态,分别代表了:下拉刷新、松开刷新、正在刷新、刷新成功、刷新失败五种样式.定义好我们的属性动画用作滑动动画,最后就是手动塞入我们的布局,代码很简单,下面上一下五中刷新状态对应的显示代码:
/** * 下拉刷新状态 */ public void pullDownToRefresh() { setRefreshState(REFRESH_BY_PULLDOWN); ll_refresh.setVisibility(View.VISIBLE); ll_ok.setVisibility(View.GONE); tv_tip.setText("下拉刷新"); getRefreshTime(); RotateAnimation anim1 = new RotateAnimation(0, 180, RotateAnimation.RELATIVE_TO_SELF, 0.5f, RotateAnimation.RELATIVE_TO_SELF, 0.5f); anim1.setDuration(300); anim1.setFillAfter(true); iv_refresh.clearAnimation(); iv_refresh.startAnimation(anim1); pb_refresh.setVisibility(View.GONE); iv_refresh.setVisibility(View.VISIBLE); Log.i("下拉刷新","下拉刷新"); } /** * 松开刷新状态 */ public void pullUpToRefresh() { setRefreshState(REFRESH_BY_RELEASE); ll_refresh.setVisibility(View.VISIBLE); ll_ok.setVisibility(View.GONE); tv_tip.setText("松开刷新"); getRefreshTime(); iv_refresh.setImageDrawable(mContext.getResources().getDrawable(R.mipmap.pull_up)); RotateAnimation anim1 = new RotateAnimation(180, 0, RotateAnimation.RELATIVE_TO_SELF, 0.5f, RotateAnimation.RELATIVE_TO_SELF, 0.5f); anim1.setDuration(300); anim1.setFillAfter(true); iv_refresh.clearAnimation(); iv_refresh.startAnimation(anim1); pb_refresh.setVisibility(View.GONE); iv_refresh.setVisibility(View.VISIBLE); Log.i("松开刷新", "松开刷新"); } /** * 正在刷新状态 */ public void refreshing() { setRefreshState(REFRESHING); ll_refresh.setVisibility(View.VISIBLE); ll_ok.setVisibility(View.GONE); tv_tip.setText("正在刷新......"); getRefreshTime(); SPUtil.getInstance(mContext).setRefreshTime("MyMobile", "" + DateUtils.getDate(DateUtils.MM_DD_HH_MM, System.currentTimeMillis())); iv_refresh.clearAnimation(); iv_refresh.setVisibility(View.GONE); pb_refresh.setVisibility(View.VISIBLE); } /** * 刷新成功状态 */ public void refreshOK() { setRefreshState(REFRESHING_SUCCESS); ll_refresh.setVisibility(View.GONE); ll_ok.setVisibility(View.VISIBLE); tv_ok.setText("刷新成功"); iv_ok.setImageDrawable(getResources().getDrawable(R.mipmap.pull_ok)); } /** * 刷新失败状态 */ public void refreshFailed() { setRefreshState(REFRESHING_FAILD); ll_refresh.setVisibility(View.GONE); ll_ok.setVisibility(View.VISIBLE); tv_ok.setText("刷新失败"); iv_ok.setImageDrawable(getResources().getDrawable(R.mipmap.pull_failure)); } public void getRefreshTime(){ String time = SPUtil.getInstance(mContext).getRefreshTime("MyMobile"); if (time == null || "".equals(time)) { tv_time.setVisibility(View.GONE); tv_tip.setGravity(Gravity.CENTER | Gravity.LEFT); ll_refresh.setGravity(Gravity.CENTER); } else { tv_time.setVisibility(View.VISIBLE); ll_refresh.setGravity(Gravity.CENTER | Gravity.LEFT); tv_time.setText("上次刷新:" + time); tv_tip.setGravity(Gravity.BOTTOM|Gravity.LEFT); } }
五种刷新状态对应五种不同的布局,简单明了!
二、下拉刷新的原理以及逻辑实现
首先我们介绍一下我们的刷新控件的原理:
- 监听滑动手势,使用LayoutParams的topMargin属性,动态改变topMargin的值达到滑动效果
- 通过滑动的高度判断当前的状态为哪种刷新状态
- 滑动结束,通过属性动画收回,回到初始样式
原理很简单,难点在于滑动手势的判断,废话不多说,先上一下滑动手势的代码:
@Override public boolean onTouchEvent(MotionEvent event) { int y = (int) event.getRawY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //记录下y坐标 lastY = y; break; case MotionEvent.ACTION_MOVE: //y移动坐标 int m = y - lastY; doMovement(m); //记录下此刻y坐标 this.lastY = y; break; case MotionEvent.ACTION_UP: fling(); break; } return true; } /** * 下拉move事件处理 * * @param moveY */ private void doMovement(int moveY) { LayoutParams lp = (LayoutParams) refreshView.getLayoutParams(); float f1 = lp.topMargin; int i = (int) (f1 + moveY * 0.4F); if (i >= refreshTargetTop) {//如果下拉大于-60dp的高度,动态刷新子视图 lp.topMargin = i; refreshView.setLayoutParams(lp); refreshView.invalidate(); invalidate(); } if (lp.topMargin > 0) {//松开刷新状态 if(refreshState!=REFRESH_BY_RELEASE) { pullUpToRefresh(); setRefreshState(REFRESH_BY_RELEASE); } } else {//下拉刷新状态 if(refreshState!=REFRESH_BY_PULLDOWN) { setRefreshState(REFRESH_BY_PULLDOWN); pullDownToRefresh(); } } }
在这里我们设置了一个0.4的滑动阻力值,在手势滑动的时候,我们通过累加topMargin的值从而达到下拉的目的,如果下拉大于-60dp(即我们一开始设置的下拉刷新头部高度),则进入刷新状态,动态改变页面.同样原理我们可以判断滑动的高度是向下还是向上,从而进行下拉刷新和松开刷新的判断,在这里,博主在改变状态之前先加入判断,这样可以过滤掉很多不必要的点,从而使我们的刷新头部箭头动画流畅,不会导致状态混乱问题.
到目前为止,我们的刷新控件基本上已经可以下拉和上拉了,怎么样,原理是不是很简单,最后,我们来看一下核心代码,就是手指离开后的处理:
/** * up事件处理 */ private void fling() { LayoutParams lp = (LayoutParams) refreshView.getLayoutParams(); if (lp.topMargin > 0) {//拉到了触发可刷新事件 refresh(); } else {//收回 animRefreshView(lp.topMargin,refreshTargetTop,300); } } private void refresh() { LayoutParams lp = (LayoutParams) this.refreshView.getLayoutParams(); int i = lp.topMargin; animRefreshView(i,0,200); refreshing(); if (refreshListener != null) { refreshListener.onRefresh(); setRefreshState(REFRESHING); } } /** * 从开始位置滑动到结束位置 * * @param startHeight * @param endHeight */ public void animRefreshView(final int startHeight,final int endHeight,int duration){ anim.start(); anim.setDuration(duration); anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation){ float cVal = (Float) animation.getAnimatedValue(); LayoutParams lp = (LayoutParams)refreshView.getLayoutParams(); int k =startHeight+(int)(cVal*(endHeight-startHeight)); lp.topMargin = k; refreshView.setLayoutParams(lp); refreshView.invalidate(); invalidate(); } }); }
首先我们判断是否拉到了可触发刷新的高度,如果触发到了,即显示刷新状态,开启收回动画,因为当前用户很可能滑动到了超过刷新头的高度,这时候我们需要先收回到刷新的高度,即屏幕中显示正在刷新时候的样式.重点在于属性动画,其实这里博主之前没有使用属性动画,而是使用了Scroller滑动器来实现收回,虽然效果大差不差,但是滑动和收回的感觉总感觉不是那么的平滑,所以我首先想到的是用属性动画来收回,如果有不熟悉属性动画的朋友们,可以自行百度一下,使用很简单,有许多的动画监听和回调,它可以返回当前每一帧的offset,然后通过改变某个状态值来刷新整个页面,上面的addUpdateListener就是它的一个动画监听方法,如何得到我们的offset呢,很简单,只需要:
(Float) animation.getAnimatedValue()
即可,我们得到了偏移量之后就可以通过当前的高度和要回到的高度来动态设置topMargin,从而达到平滑的收回
到此,我们的刷新控件完成了四分之三,是不是觉得很简单呢!
三、刷新结束回调以及使用
当然了,因为我们的控件是下拉刷新,当然少不了刷新时候的回调,当刷新完成的时候,我们还要收回我们的刷新控件,代码很简单, 在这里,博主就直接贴代码了:
/** * 刷新监听接口 * * @author Nono */ public interface RefreshListener { void onRefresh(); } /** * 设置刷新回调 * @param listener */ public void setRefreshListener(RefreshListener listener) { this.refreshListener = listener; } /** * 结束刷新事件 */ public void finishRefresh(boolean isOK) { LayoutParams lp = (LayoutParams) this.refreshView.getLayoutParams(); final int i = lp.topMargin; if (isOK) { refreshOK(); } else { refreshFailed(); } if(!anim.isRunning()&&refreshState!=REFRESHING){ new Handler().postDelayed(new Runnable(){ public void run() { animRefreshView(i,refreshTargetTop,500); } }, 300); } }
其中结束刷新事件中我们添加了延时,因为刷新成功或者失败要给用户一个反馈,所以我们需要延时0.5秒给用户.当我们的刷新完成后,只需要调用一下finishRefresh的方法,告诉控件滑动完成了,可以收回了.
使用很简单,因为我们是继承LinearLayout的,所以我们可以直接在布局中套在ScrollView上,使用的时候直接findViewByID绑定实现刷新方法即可下面贴上布局代码:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <com.ypx.jiehunle.ypx_bezierqqrefreshdemo.YPXRefreshView android:id="@+id/refreshableView1" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:visibility="visible"> <ScrollView android:id="@+id/scrollView1" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:id="@+id/ll_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > </LinearLayout> </ScrollView> </com.ypx.jiehunle.ypx_bezierqqrefreshdemo.YPXRefreshView> </RelativeLayout>
使用时的Activity代码:
public class MainActivity extends Activity { YPXRefreshView refreshableView; LinearLayout layout; final int SUCCESS = 1; final int FAILED = 0; @SuppressLint("HandlerLeak") Handler handler = new Handler() { public void handleMessage(android.os.Message msg) { switch (msg.what) { case SUCCESS: refreshableView.finishRefresh(true); TextView textView = new TextView(MainActivity.this); textView.setTextColor(Color.BLACK); textView.setTextSize(20); textView.setText("这是刷新的文本"); layout.addView(textView,0); break; case FAILED: refreshableView.finishRefresh(false); break; default: break; } }; }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); initData(); } private void initData() { layout.removeAllViews(); for (int i = 0; i < 50; i++) { final TextView textView = new TextView(MainActivity.this); textView.setTextColor(Color.BLACK); textView.setTextSize(20); textView.setText("这是第" + i + "个文本"); textView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { Toast.makeText(MainActivity.this,textView.getText(),0).show(); } }); layout.addView(textView); } refreshableView.setRefreshListener(new YPXRefreshView.RefreshListener() { @Override public void onRefresh() { handler.postDelayed(new Runnable() { @Override public void run() { handler.sendEmptyMessage(SUCCESS); } }, 500); } }); } private void initView() { refreshableView = (YPXRefreshView) findViewById(R.id.refreshableView1); layout = (LinearLayout) findViewById(R.id.ll_layout); refreshableView.setRefreshEnabled(true); } }
到这里,我们的刷新控件差不多完成了四分之三点五了,什么?还没有结束吗?当然,因为题目是打造通用的刷新控件,所以我们还有最后的环节!
四、事件拦截处理,达到通用效果
什么是通用,因为我们的控件是下拉刷新,所以应该支持所有的可滑动布局才对,这就涉及到了事件分发机制,还不了解的小伙伴们,可以自行去补习一下,这里博主就不赘述了.言归正传,既然我们要实现通用的刷新,必然要进行事件拦截,首先想到的就是重写ViewGroup的onInterceptTouchEvent方法了,那么我们研究一下什么时候需要拦截,什么时候不需要拦截呢?
其实很简单,当我们内部的滑动控件(ListView或ScrollView等)滑动到最顶部的时候,这时候我们需要触发下拉刷新,反之则不拦截,给子View自己处理,当然,在进行这一切的时候,我们要先判断是否存在子View以及判断子View是继承哪一种滑动布局,在这里博主只是简单的给个例子.所以如果要实现更多的滑动布局刷新,要添加判断,比如WebView、GridView、RecyclerView等,判断它们是否滑动到顶部即可,下面上博主的代码:
@Override public boolean onInterceptTouchEvent(MotionEvent e) { if(!isRefreshEnabled){ return false; } int action = e.getAction(); int y = (int) e.getRawY(); switch (action) { case MotionEvent.ACTION_DOWN: lastY = y; break; case MotionEvent.ACTION_MOVE: if (y > lastY && canScroll()) { return true; } //记录下此刻y坐标 this.lastY = y; break; } return false; } private boolean canScroll() { View childView; if (getChildCount() > 1) { childView = this.getChildAt(1); if (childView instanceof ListView) { int top = ((ListView) childView).getChildAt(0).getTop(); int pad = ((ListView) childView).getListPaddingTop(); if ((Math.abs(top - pad)) < 3 && ((ListView) childView).getFirstVisiblePosition() == 0) { return true; } else { return false; } } else if (childView instanceof ScrollView) { if (((ScrollView) childView).getScrollY() == 0) { return true; } else { return false; } }else if (childView instanceof WebView) { if (((WebView) childView).getScrollY() == 0) { return true; } else { return false; } }else if (childView instanceof GridView) { int top = ((GridView) childView).getChildAt(0).getTop(); int pad = ((GridView) childView).getListPaddingTop(); if ((Math.abs(top - pad)) < 3 && ((GridView) childView).getFirstVisiblePosition() == 0) { return true; } else { return false; } }else if (childView instanceof RecyclerView) { RecyclerView.LayoutManager manager=((RecyclerView)childView).getLayoutManager(); int top=0; if(manager instanceof LinearLayoutManager){ top = ((LinearLayoutManager)manager).findFirstVisibleItemPosition(); }else if(manager instanceof StaggeredGridLayoutManager){ top = ((StaggeredGridLayoutManager)manager).findFirstVisibleItemPositions(null)[0]; } if(((RecyclerView)childView).getChildAt(0).getY()==0 &&top==0){ return true; } else { return false; } } } return false; }
可以看到在canScroll函数中博主添加了很多判断, 这里只要判断了字View类型是否是滑动布局类型,其中包括,ScrollView、ListView、WebView、GridView、RecyclerView等,其中判断很简单,就是当前用户如果滑动到顶部,则交给外部下拉刷新处理,其余则放给字View处理.如果用户有自己自定义的滑动布局的话,可以在此基础上手动添加即可.到这里,总算完成了我们的刷新控件.
五、总结
总的来说,博主实现的下拉刷新还是非常简单易懂的,滑动流畅,使用简单,当然,这不是博主的目的,正如前言所说,博主的目的是为了实现仿IOS的QQ下拉刷新,本篇下拉刷新只是实现的第一步,下一步将会在下一篇博客(安卓仿IOS版QQ下拉刷新(二) ——二维贝塞尔远没有你想的那么复杂)中给大家带来一点关于贝塞尔曲线的实现,期待的朋友们欢迎支持一下博主哦~
感谢大家的支持,谢谢!
QQ:313930500
下载地址:http://download.csdn.net/detail/qq_16674697/9741375
转载请注明出处~谢谢~