x寫控制項挺麻煩的,因為有很多細節要處理好,列表控制項使用太頻繁了,網上也各種自定義的方法,一般的listview自定義肯定會聯想到加個頭部,然後監聽事件加動畫,其實方式很多種,今天記錄的方式是另外一種方式,個人覺得復用性更強,寫好了可以通用,思路就是在不動原列表控制項的情況下給它上面套個殼,然後讓殼來操 ...
x寫控制項挺麻煩的,因為有很多細節要處理好,列表控制項使用太頻繁了,網上也各種自定義的方法,一般的listview自定義肯定會聯想到加個頭部,然後監聽事件加動畫,其實方式很多種,今天記錄的方式是另外一種方式,個人覺得復用性更強,寫好了可以通用,思路就是在不動原列表控制項的情況下給它上面套個殼,然後讓殼來操作刷新顯示,這樣的話是不是以後要用的時候加個殼就行了,而且我可以基本上不管裡面的控制項是什麼。
下載地址: http://download.csdn.net/detail/u010864175/9805339
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context="cn.com.listwebview.demo.MainActivity"> <cn.com.listwebview.demo.ListRefreshLayout android:id="@+id/refreshLayout" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#f4a148" android:orientation="vertical" > <ListView android:id="@+id/listview" android:layout_width="match_parent" android:layout_height="match_parent" android:background="#dff19a" android:divider="#595958"/> <!--<android.support.v7.widget.RecyclerView--> <!--android:id="@+id/recyclerview"--> <!--android:divider="#ffff0000"--> <!--android:dividerHeight="10dp"--> <!--android:background="#dff19a"--> <!--android:layout_width="match_parent"--> <!--android:layout_height="match_parent" />--> </cn.com.listwebview.demo.ListRefreshLayout> </LinearLayout>View Code
MainActivity很簡單,啥都不用做,按著 RecyclerView或者listview載入數據的方式載入就好了MainActivity
ListRefreshLayout
重點都在這個殼裡了,註釋非常詳細,因為殼是父容器,所以不用傳遞視圖進去,這樣也會產生依賴關係,界面開始顯示listview或者recyclerview列表,監聽時間滑動,然後判斷滑動的程度,距離,
是否在刷新中等等,接著通過更新MarginTop來展示頭部的刷新,而頭部都是在載入事件裡面動態添加進去的,因為效果都是在殼裡實現的,所以不影響列表控制項添加頭部腳部,以後也可以更換配置頭部樣
式,這樣xml里也不用特地的為這個控制項去寫特定的佈局,不然這種做法就失去了意義了,和直接添加頭部也沒什麼大區別了,顯示出了頭部根據你滑動的狀態來控制動畫,然後已經運行了動畫就不能讓它
在更新top了,不然重覆刷新,接著抬起操作,此處我直接設置的兩秒完成,實際是訪問回調介面停止的。
package cn.com.listwebview.demo; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.Rect; import android.support.v4.view.ViewCompat; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.StaggeredGridLayoutManager; import android.util.AttributeSet; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import android.view.ViewGroup; import android.view.animation.Animation; import android.view.animation.LinearInterpolator; import android.view.animation.RotateAnimation; import android.webkit.WebView; import android.widget.AbsListView; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.ListView; import android.widget.ProgressBar; import java.lang.reflect.Field; import cn.com.listwebview.demo.utils.DensityUtil; /** * Created by LiuZhen on 2017/3/24. */ public class ListRefreshLayout extends FrameLayout{ private String TAG = "TwinklingRefreshLayout"; private int downY;// 按下時y軸的偏移量 private final static float RATIO = 3f; //頭部的高度 protected int mHeadHeight; //頭部layout protected FrameLayout mHeadLayout;//頭部父容器 private HeaderView mHeadView;//頭部 protected FrameLayout mFootLayout;//頭部父容器 private ImageView ivArrow_left,ivArrow_right; //頭佈局的剪頭 private ProgressBar mProgressBar; // 底佈局的進度條 private Animation upAnimation;// 向上旋轉的動畫 private Animation downAnimation;// 向下旋轉的動畫 private final int DOWN_PULL_REFRESH = 0;// 下拉刷新狀態 private final int RELEASE_REFRESH = 1;// 鬆開刷新 private final int REFRESHING = 2;// 正在刷新中 private final int END = 3;// 正在刷新中 private int currentState = DOWN_PULL_REFRESH;// 頭佈局的狀態: 預設為下拉刷新狀態 private View list;//子節點中的listview視圖 private LayoutParams listParam,footParam;//用於控制下拉動畫展示 private boolean isLoadingMore = false;// 是否進入載入狀態,防止多次重覆的啟動 private boolean isStart = false;//表示正在載入刷新中,還沒停止 private boolean isTop = false,isBottom = false; private int mTouchSlop; public ListRefreshLayout(Context context) { this(context, null, 0); } public ListRefreshLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ListRefreshLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TwinklingRefreshLayout, defStyleAttr, 0); try { mHeadHeight = a.getDimensionPixelSize(R.styleable.TwinklingRefreshLayout_tr_head_height, DensityUtil.dp2px(context, 40)); } finally { a.recycle(); } mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); addHeader(); init(); } private void addHeader() { FrameLayout headViewLayout = new FrameLayout(getContext()); LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,mHeadHeight); this.addView(headViewLayout,layoutParams); mHeadLayout = headViewLayout; } private void init(){ initAnimation(); } @Override protected void onFinishInflate() {//佈局載入成xml時觸發 super.onFinishInflate(); if (mHeadView == null) setHeaderView(new HeaderView(getContext())); setFootView(); if (list == null) { list = getChildAt(1); listParam = (LayoutParams) list.getLayoutParams(); list.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { return onFingerTouch(event); } }); //下麵是原先的方法,比較簡單,也可以達到同樣的效果,思路也是需要判斷類型,畢竟每個列表控制項的判斷top bottom方式不同,只是此處需要用具體的list對象來監聽,所以採用另外的方式來判斷 // list.setOnScrollListener(new AbsListView.OnScrollListener() { // @Override // public void onScrollStateChanged(AbsListView view, int scrollState) { // // } // // @Override // public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { // isTop = false; // isBottom = false; // //判斷頂部底部 // if (firstVisibleItem == 0) { // Log.d(TAG, "滾動到頂部"); // isTop = true; // isBottom = false; // } else if ((firstVisibleItem + visibleItemCount) == totalItemCount) { // Log.d(TAG, "滾動到底部"); // isTop = false; // isBottom = true; // } // } // }); } } /** * 設置頭部View */ public void setHeaderView(final HeaderView headerView) { if (headerView != null) { post(new Runnable() { @Override public void run() { mHeadLayout.removeAllViewsInLayout(); mHeadLayout.addView(headerView.getView()); View view = LayoutInflater.from(getContext()).inflate(R.layout.item_head_progress,null); // mProgressBar = (ProgressBar) view.findViewById(R.id.pb_listview_header); ivArrow_left = (ImageView) view.findViewById(R.id.iv_listview_header_arrow_left) ; ivArrow_right = (ImageView) view.findViewById(R.id.iv_listview_header_arrow_right) ; mHeadLayout.addView(view); } }); mHeadView = headerView; } } /** * 設置尾部View */ public void setFootView() { footParam = new LayoutParams(LayoutParams.MATCH_PARENT,mHeadHeight); FrameLayout footViewLayout = new FrameLayout(getContext());//底部佈局 this.addView(footViewLayout,footParam); this.mFootLayout = footViewLayout; mFootLayout.setBackgroundColor(Color.BLACK); footParam.gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL; footParam.setMargins(0,0,0,-mHeadHeight); mProgressBar = new ProgressBar(getContext(),null,android.R.attr.progressBarStyleSmallInverse); mFootLayout.addView(mProgressBar); } public boolean onFingerTouch(MotionEvent ev) { isTop = isViewToTop(list,mTouchSlop); isBottom = isViewToBottom(list,mTouchSlop); // Log.e(TAG,"isTop "+isTop+" isBottom "+isBottom); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN : currentState = REFRESHING; downY = (int) ev.getY(); break; case MotionEvent.ACTION_MOVE : if (!isTop && !isBottom)//沒有到頂,無需計算操作 break; int moveY = (int) ev.getY(); int diff = (int) (((float)moveY - (float)downY) / RATIO); // int paddingTop = -mHeadLayout.getHeight() + diff; int paddingTop = diff; if (diff>0 && isTop) { //向下滑動多少後開始啟動刷新 if (paddingTop >= 200 && currentState == DOWN_PULL_REFRESH) { // 完全顯示了. // Log.i(TAG, "鬆開刷新 RELEASE_REFRESH"); currentState = RELEASE_REFRESH; refreshHeaderView(); start(); } else if (currentState == REFRESHING) { // 沒有顯示完全 // Log.i(TAG, "下拉刷新 DOWN_PULL_REFRESH"); currentState = DOWN_PULL_REFRESH; refreshHeaderView(); } if (paddingTop <= 400 && !isStart) {//已經處於運行刷新狀態的時候禁止設置 listParam.setMargins(0, paddingTop, 0, 0); list.setLayoutParams(listParam); } }else if (isBottom){ //限制上滑時不能超過底部的寬度,不然會超出邊界 if (paddingTop <= -50 && paddingTop >= -mHeadHeight && !isStart) {//已經處於運行刷新狀態的時候禁止設置 listParam.setMargins(0, 0, 0, -paddingTop); footParam.setMargins(0,0,0,-paddingTop-mHeadHeight); list.setLayoutParams(listParam); } if (paddingTop <= -mHeadHeight) isLoadingMore = true; } // Log.i(TAG,"paddingTop "+paddingTop); break; case MotionEvent.ACTION_UP : if (isLoadingMore){ isLoadingMore = false; postDelayed(new Runnable() { @Override public void run() { // Log.i(TAG, "停止 END"); // currentState = END; refreshHeaderView(); listParam.setMargins(0, 0, 0, 0); footParam.setMargins(0,0,0,-mHeadHeight); list.setLayoutParams(listParam); stop(); } },2000); }else{ if (!isStart){ // 隱藏頭佈局 listParam.setMargins(0, 0,0,0); footParam.setMargins(0,0,0,-mHeadHeight); list.setLayoutParams(listParam); } } // Log.i(TAG, "鬆開 REFRESHING"); currentState = REFRESHING; break; default : break; } return super.onTouchEvent(ev); } /** * 初始化動畫 */ private void initAnimation() { /* * Animation.RELATIVE_TO_SELF 相對於自身的動畫 * Animation.RELATIVE_TO_PARENT 相對於父控制項的動畫 * 0.5f,表示在控制項自身的 x,y的中點坐標處,為動畫的中心。 * * 設置動畫的變化速率 * setInterpolator(newAccelerateDecelerateInterpolator()):先加速,後減速 * setInterpolator(newAccelerateInterpolator()):加速 * setInterpolator(newDecelerateInterpolator()):減速 * setInterpolator(new CycleInterpolator()):動畫迴圈播放特定次數,速率改變沿著正弦曲線 * setInterpolator(new LinearInterpolator()):勻速 */ upAnimation = new RotateAnimation(0f, -180f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); upAnimation.setInterpolator(new LinearInterpolator()); upAnimation.setDuration(700); upAnimation.setFillAfter(true); // 動畫結束後, 停留在結束的位置上 downAnimation = new RotateAnimation(-180f, -360f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f); downAnimation.setInterpolator(new LinearInterpolator());//這句話可以不寫,預設勻速 downAnimation.setDuration(700); downAnimation.setFillAfter(true); // 動畫結束後, 停留在結束的位置上 } /** * 根據currentState刷新頭佈局的狀態 */ private void refreshHeaderView() { switch (currentState) { case DOWN_PULL_REFRESH : // 下拉刷新狀態 // tvState.setText("下拉刷新"); ivArrow_left.startAnimation(downAnimation); // 執行向下旋轉 ivArrow_right.startAnimation(downAnimation); // 執行向下旋轉 break; case RELEASE_REFRESH : // 鬆開刷新狀態 // tvState.setText("鬆開刷新"); ivArrow_left.startAnimation(upAnimation);// 執行向上旋轉 ivArrow_right.startAnimation(upAnimation);// 執行向上旋轉 break; case REFRESHING : // 正在刷新中狀態 ivArrow_left.clearAnimation(); ivArrow_right.clearAnimation(); // tvState.setText("正在刷新中..."); break; default : break; } } public static boolean isViewToTop(View view,int mTouchSlop){ if (view instanceof AbsListView) return isAbsListViewToTop((AbsListView) view); if (view instanceof RecyclerView) return isRecyclerViewToTop((RecyclerView) view); return (view != null && Math.abs(view.getScrollY()) <= 2 * mTouchSlop); } public static boolean isViewToBottom(View view,int mTouchSlop){ if (view instanceof AbsListView) return isAbsListViewToBottom((AbsListView) view); if (view instanceof RecyclerView) return isRecyclerViewToBottom((RecyclerView) view); // if (view instanceof WebView) return isWebViewToBottom((WebView) view,mTouchSlop); // if (view instanceof ViewGroup) return isViewGroupToBottom((ViewGroup) view); return false; } public static boolean isAbsListViewToTop(AbsListView absListView) { if (absListView != null) { int firstChildTop = 0; if (absListView.getChildCount() > 0) { // 如果AdapterView的子控制項數量不為0,獲取第一個子控制項的top firstChildTop = absListView.getChildAt(0).getTop() - absListView.getPaddingTop(); } if (absListView.getFirstVisiblePosition() == 0 && firstChildTop == 0) { return true; } } return false; } public static boolean isRecyclerViewToTop(RecyclerView recyclerView) { if (recyclerView != null) { RecyclerView.LayoutManager manager = recyclerView.getLayoutManager(); if (manager == null) { return true; } if (manager.getItemCount() == 0) { return true; } if (manager instanceof LinearLayoutManager) { LinearLayoutManager layoutManager = (LinearLayoutManager) manager; int firstChildTop = 0; if (recyclerView.getChildCount() > 0) { // 處理item高度超過一屏幕時的情況 View firstVisibleChild = recyclerView.getChildAt(0); if (firstVisibleChild != null && firstVisibleChild.getMeasuredHeight() >= recyclerView.getMeasuredHeight()) { if (android.os.Build.VERSION.SDK_INT < 14) { return !(ViewCompat.canScrollVertically(recyclerView, -1) || recyclerView.getScrollY() > 0); } else { return !ViewCompat.canScrollVertically(recyclerView, -1); } } // 如果RecyclerView的子控制項數量不為0,獲取第一個子控制項的top // 解決item的topMargin不為0時不能觸發下拉刷新 View firstChild = recyclerView.getChildAt(0); RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) firstChild.getLayoutParams(); firstChildTop = firstChild.getTop() - layoutParams.topMargin - getRecyclerViewItemTopInset(layoutParams) - recyclerView.getPaddingTop(); } if (layoutManager.findFirstCompletelyVisibleItemPosition() < 1 && firstChildTop == 0) { return true; } } else if (manager instanceof StaggeredGridLayoutManager) { StaggeredGridLayoutManager layoutManager = (StaggeredGridLayoutManager) manager; int[] out = layoutManager.findFirstCompletelyVisibleItemPositions(null); if (out[0] < 1) { return true; } } } return false; } /** * 通過反射獲取RecyclerView的item的topInset * * @param layoutParams * @return */ private static int getRecyclerViewItemTopInset(RecyclerView.LayoutParams layoutParams) { try { Field field = RecyclerView.LayoutParams.class.getDeclaredField("mDecorInsets"); field.setAccessible(true); // 開發者自定義的滾動監聽器 Rect decorInsets = (Rect) field.get(layoutParams); return decorInsets.top; } catch (Exception e) { e.printStackTrace(); } return 0; } public static boolean isAbsListViewToBottom(AbsListView absListView) { if (absListView != null && absListView.getAdapter() != null && absListView.getChildCount() > 0 && absListView.getLastVisiblePosition() == absListView.getAdapter().getCount() - 1) { View lastChild = absListView.getChildAt(absListView.getChildCount() - 1); return lastChild.getBottom() <= absListView.getMeasuredHeight(); } return false; } public static boolean isRecyclerViewToBottom(RecyclerView recyclerView) { if (recyclerView != null) { RecyclerView.LayoutManager manager = recyclerView.getLayoutManager(); if (manager == null || manager.getItemCount() == 0) { return false; } if (manager instanceof LinearLayoutManager) { // 處理item高度超過一屏幕時的情況 View lastVisibleChild = recyclerView.getChildAt(recyclerView.getChildCount() - 1); if (lastVisibleChild != null && lastVisibleChild.getMeasuredHeight() >= recyclerView.getMeasuredHeight()) { if (android.os.Build.VERSION.SDK_INT < 14) { return !(ViewCompat.canScrollVertically(recyclerView, 1) || recyclerView.getScrollY() < 0); } else { return !ViewCompat.canScrollVertically(recyclerView, 1); } } LinearLayoutManager layoutManager = (LinearLayoutManager) manager; if (layoutManager.findLastCompletelyVisibleItemPosition() == layoutManager.getItemCount() - 1) { return true; } } else if (manager instanceof StaggeredGridLayoutManager) { StaggeredGridLayoutManager layoutManager = (StaggeredGridLayoutManager) manager; int[] out = layoutManager.findLastCompletelyVisibleItemPositions(null); int lastPosition = layoutManager.getItemCount() - 1; for (int position : out) { if (position == lastPosition) { return true; } } } } return false; } public void start(){ isLoadingMore = true; isStart = true; mHeadView.onPullingDown(0); mHeadView.startAnim(); } public void stop(){ isLoadingMore = false; isStart = false; mHeadView.reset(); } }
有了頭部還得要漂亮的動畫啊,增強體驗,動畫就隨自己配置了,本身控制項的動畫就是兩個箭頭我這裡是加了個自定義的視圖來做動畫,繪製了幾個圓圈,可以根據自己的需要調製,畢竟頭部是一個小的容器佈局,展示的頭部也都是在這個佈局裡面,所以可以任意搭配。
底部就簡單的以同樣的思路來做的了,同樣是通過滑動上拉來展示刷新的底部和啟動動畫(個人覺得滑動後載入體驗好點,不太喜歡直接滑到頂就顯示載入了),底部實現簡單了點,就添加了一個底部的佈局(這裡添加是載入好xml佈局後在添加的,因為是動態佈局會添加在佈局上面),然後裡面加了個簡單的progress。
HeaderView
package cn.com.listwebview.demo; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.util.AttributeSet; import android.view.View; import android.view.animation.DecelerateInterpolator; import cn.com.listwebview.demo.utils.DensityUtil; /** * Created by LiuZhen on 2017/3/28. */ public class HeaderView extends View { private Paint mPath; ValueAnimator animator1, animator2; private float r; float fraction1; float fraction2; boolean animating = false; private int num = 5; private int cir_x = 0; public HeaderView(Context context) { this(context, null, 0); } public HeaderView(Context context, AttributeSet attrs) { this(context, attrs,0); } public HeaderView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { r = DensityUtil.dp2px(getContext(), 6); mPath = new Paint(); mPath.setAntiAlias(true); mPath.setColor(Color.rgb(114, 114, 114)); animator1 = ValueAnimator.ofFloat(1f, 1.2f, 1f, 0.8f);//從左到右過渡 animator1.setDuration(800); animator1.setInterpolator(new DecelerateInterpolator());//DecelerateInterpolator減速插補器 animator1.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { fraction1 = (float) animation.getAnimatedValue();//監聽動畫運動值 invalidate(); } }); animator1.setRepeatCount(ValueAnimator.INFINITE);//設置重覆次數為無限次 animator1.setRepeatMode(ValueAnimator.REVERSE);//RESTART是直接重新播放 animator2 = ValueAnimator.ofFloat(1f, 0.8f, 1f, 1.2f); animator2.setDuration(800); animator2.setInterpolator(new DecelerateInterpolator()); animator2.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { fraction2 = (float) animation.getAnimatedValue(); } }); animator2.setRepeatCount(ValueAnimator.INFINITE); animator2.setRepeatMode(ValueAnimator.REVERSE); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int w = getMeasuredWidth() / num - 10; for (int i = 0; i < num; i++) { if (animating) { switch (i) { case 0: mPath.setAlpha(105); mPath.setColor(getResources().getColor(R.color.Yellow)); canvas.drawCircle(getMeasuredWidth() / 2 - cir_x * 2 - 2 * w / 3 * 2, getMeasuredHeight() / 2, r * fraction2, mPath); break; case 1: mPath.setAlpha(145); mPath.setColor(getResources().getColor(R.color.Green)); canvas.drawCircle(getMeasuredWidth() / 2 - cir_x * 1 - w / 3 * 2, getMeasuredHeight() / 2, r * fraction2, mPath); break; case 2: mPath.setAlpha(255); mPath.setColor(getResources().getColor(R.color.Blue)); canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, r * fraction1, mPath); break; case 3: mPath.setAlpha(145); mPath.setColor(getResources().getColor(R.color.Orange)); canvas.drawCircle(getMeasuredWidth() / 2 + cir_x * 1 + w / 3 * 2, getMeasuredHeight() / 2, r * fraction2, mPath); break; case 4: mPath.setAlpha(105); mPath.setColor(getResources().getColor(R.color.Yellow)); canvas.drawCircle(getMeasuredWidth() / 2 + cir_x * 2 + 2 * w / 3 * 2, getMeasuredHeight() / 2, r * fraction2, mPath); break; } } else { switch (i) { case 0: mPath.setAlpha(105); mPath.setColor(getResources().getColor(R.color.Yellow)); canvas.drawCircle(getMeasuredWidth() / 2 - cir_x * 2 - 2 * w / 3 * 2, getMeasuredHeight() / 2, r, mPath); break; case 1: mPath.setAlpha(145); mPath.setColor(getResources().getColor(R.color.Green)); canvas.drawCircle(getMeasuredWidth() / 2 - cir_x * 1 - w / 3 * 2, getMeasur