标签:标记 ... auth styles ica 计算 color osi appear
因为公司人员变动原因,导致了博主四个月没有动安卓,一直在做IOS开发,如今接近年前,终于可以花一定的时间放在安卓上了.好了,废话不多说,今天我们要带来的效果是苹果版本的QQ下拉刷新.首先看一下目标效果以及demo效果:
因为此效果实现的步骤较多,所以今天博主要实现以上效果的第一步——打造一个通用的下拉刷新控件,具体效果如下:
GIF图片比较大,还希望读者能耐心等待一下下从效果图中可以看出,我们的下拉刷新的滑动还是很流畅的,可能大多数开发者用的是XListview或者PullToRefresh控件,在此博主本着能造轮子就造轮子的原则,打算自己打造一个自己喜欢的通用下拉刷新控件;下面就由博主来说明一下此控件是如何完成的:
首先我们得准备好我们的刷新头部的布局,布局稍微复杂一点,分两层.一个是正在刷新时候的布局,一个是刷新完成的布局,在这里我用的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>
接下来,我们开始编写自定义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); } }
五种刷新状态对应五种不同的布局,简单明了!
首先我们介绍一下我们的刷新控件的原理:
原理很简单,难点在于滑动手势的判断,废话不多说,先上一下滑动手势的代码:
@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(); } } }
到目前为止,我们的刷新控件基本上已经可以下拉和上拉了,怎么样,原理是不是很简单,最后,我们来看一下核心代码,就是手指离开后的处理:
/** * 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(); } }); }
(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); } }
使用很简单,因为我们是继承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>
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
转载请注明出处~谢谢~
Android仿苹果版QQ下拉刷新实现(一) ——打造简单平滑的通用下拉刷新控件
标签:标记 ... auth styles ica 计算 color osi appear
原文地址:http://www.cnblogs.com/teamblog/p/7347468.html