通過ViewGroup實現下拉重新整理和上拉載入,2018/2/12 06
阿新 • • 發佈:2019-02-08
為了重新瞭解一下自定義ViewGroup,自己實現了一個下拉重新整理view,衝突的解決Recyclerview滾動到底部和頂部的處理全部放在了父view 中,滾動實現使用的是Scroller,所以使整個控制元件還有類似ios的彈性效果,程式碼很簡單,使用也很簡單,重新整理的頭佈局和腳佈局都可以在佈局檔案中直接新增,處理。
其中內容也可以不是Recyclerview,直接寫在佈局裡就行,佈局效果類似LinearLayout 但只能是豎直方向堆疊。
初始化佈局等程式碼:
private Scroller mScroller;
private RecyclerView mRecyclerView;
private RefreshListener mListener;
private int mTotalHeight; //子view加在一起總高度
private HashMap<Integer,Integer> mViewMarginTop; //所有子view marginTop距離
private HashMap<Integer,Integer> mViewMarginBottom;//所有子view marginBottom距離
private float mStartY;//手指落下位置 移動之後更新
private float mStartX;
private int mMoveHeight; //移動的總高度
private int mState; // 重新整理狀態
private static int NORMAL=0; // 正常狀態
private static int REFRESH=1; // 重新整理中
private static int LOAD=2; //上拉載入中
private boolean isPull; //是否拖動中
private int mCanScrollDistance=-1; //可拖動最遠距離
private boolean mCanLoadMore;//可否上拉載入
public RefreshLayout(Context context) {
this(context,null);
}
public RefreshLayout(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public RefreshLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mViewMarginTop= new HashMap<>();
mViewMarginBottom= new HashMap<>();
mScroller = new Scroller(context);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
for (int i = 0; i < getChildCount(); i++) {
//測量每一個子
measureChild(getChildAt(i),widthMeasureSpec,heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int layoutWidth = getMeasuredWidth();
mTotalHeight=t;//初始化高度
for (int i = 0; i < getChildCount(); i++) {
View view = getChildAt(i);
if (view.getVisibility() != GONE) {
LayoutParams layoutParams = (LayoutParams) view.getLayoutParams();
int viewHeight = view.getMeasuredHeight();
int viewWidth = view.getMeasuredWidth();
if (i != 0) {
mTotalHeight += viewHeight;
if (mViewMarginTop.get(i) != null) {//總高度加上子view距離上方的距離
mTotalHeight += mViewMarginTop.get(i);
}
}
if(layoutParams.mCenter) {
int marginHorizontal = (layoutWidth - viewWidth-getPaddingLeft()-getPaddingRight()) / 2;
if(mTotalHeight>getMeasuredHeight()) {//如果子view總高度大於父控制元件總高度則最大值為父控制元件高度
int viewMarginBottom=0;
if (mViewMarginBottom.get(i) != null) {
viewMarginBottom = mViewMarginBottom.get(i);
}
if((mTotalHeight-viewHeight)>=getMeasuredHeight()) {//判斷如果子view在父控制元件高度之外 繪製的位置
view.layout(l + marginHorizontal + getPaddingLeft(), mTotalHeight - viewHeight, viewWidth + marginHorizontal + l + getPaddingLeft(), mTotalHeight);
}else {
view.layout(l + marginHorizontal + getPaddingLeft(), mTotalHeight - viewHeight, viewWidth + marginHorizontal + l + getPaddingLeft(), getMeasuredHeight() - viewMarginBottom);
}
}else{
view.layout(l + marginHorizontal + getPaddingLeft(), mTotalHeight - viewHeight, viewWidth + marginHorizontal + l + getPaddingLeft(), mTotalHeight);
}
}else{
if(mTotalHeight>getMeasuredHeight()) {//如果子view總高度大於父控制元件總高度則最大值為父控制元件高度
int viewMarginBottom=0;
if (mViewMarginBottom.get(i) != null) {
viewMarginBottom = mViewMarginBottom.get(i);
}
if((mTotalHeight-viewHeight)>=getMeasuredHeight()) {//判斷如果子view在父控制元件高度之外 繪製的位置
view.layout(l + paddingLeft, mTotalHeight - viewHeight, viewWidth + l + paddingLeft + paddingRight, mTotalHeight);
}else{
view.layout(l + paddingLeft, mTotalHeight - viewHeight, viewWidth + l + paddingLeft + paddingRight, getMeasuredHeight() - viewMarginBottom);
mTotalHeight=getMeasuredHeight() - viewMarginBottom;
}
}else{
view.layout(l + paddingLeft, mTotalHeight - viewHeight, viewWidth + l + paddingLeft + paddingRight, mTotalHeight);
}
}
if (mViewMarginBottom.get(i) != null) {//總高度加上子view距離下方的距離
mTotalHeight += mViewMarginBottom.get(i);
}
}
}
}
獲取子view屬性,和自定義屬性,其中自定義屬性需要在values資料夾下建立attrs資原始檔,僅僅只取了幾個處理了一下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="Refresh_Layout">
<attr name="center_horizontal" format="boolean" />
</declare-styleable>
</resources>
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return super.generateLayoutParams(p);
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(),attrs);
}
/**
* 自定義LayoutParams 新增自定義屬性
* 並可以獲取到子view的屬性
*/
private class LayoutParams extends ViewGroup.LayoutParams {
private boolean mCenter;
private LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
for (int i = 0; i < attrs.getAttributeCount(); i++) {
String attributeName = attrs.getAttributeName(i);
String attributeValue = attrs.getAttributeValue(i);
switch (attributeName){
case "layout_marginTop"://單位px 儲存所有子view marginTop高度
if(attributeValue.length()>2) {
mViewMarginTop.put(getChildCount(),(int) Double.parseDouble(attributeValue.substring(0, attributeValue.length() - 2)));
}
break;
case "layout_marginBottom"://單位px 儲存所有子view marginBottom高度
if(attributeValue.length()>2) {
mViewMarginBottom.put(getChildCount(),(int) Double.parseDouble(attributeValue.substring(0, attributeValue.length() - 2)));
}
break;
case "center_horizontal"://自定義屬性水平居中
mCenter = Boolean.valueOf(attributeValue);
break;
}
}
}
}
之後是觸控事件的處理:
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
if (mScroller != null && !mScroller.isFinished()) {
mScroller.abortAnimation();
}
mStartY = event.getRawY();
mStartX = event.getRawY();
super.dispatchTouchEvent(event);
return true;
case MotionEvent.ACTION_MOVE:
if(Math.abs(mMoveHeight)>mCanScrollDistance && mCanScrollDistance>0){//判斷是否到達設定的極限滾動距離
return true;
}
int round = Math.round(mStartY - event.getRawY());
int roundX = Math.round(mStartX - event.getRawX());
if(mListener!=null){
mListener.moveDy(round);
}
if(Math.abs(roundX)>Math.abs(round)){//保證橫向能夠滾動
mStartY -= round;
mStartX -= roundX;
return super.dispatchTouchEvent(event);
}
if(mRecyclerView!=null) {
if (!mRecyclerView.canScrollVertically(-1)) {//豎直方向recyclerView可否下拉
if (round < 0) {
isPull = true;
dealMove(round,event);
return true;
}
}
if (!mRecyclerView.canScrollVertically(1)) {//豎直方向recyclerView可否上拉
if (round > 0) {//上拉
isPull = true;
dealMove(round,event);
return true;
}
}
if (mState == REFRESH || isPull) {//重新整理狀態,拖動狀態下攔截,自己處理移動
dealMove(round,event);
return true;
}
if(mMoveHeight>0){//如果底部view還在顯示則下拉是從底部view開始
dealMove(round,event);
return true;
}
mStartY -= round;
mStartX -= roundX;
}else{
dealMove(round,event);
return true;
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
isPull=false;
int height = getChildAt(0).getHeight();
if(mMoveHeight<0 || mState == REFRESH) {
if (-mMoveHeight > height && mState == NORMAL) { // 正常狀態下下拉超過第一個view高度,改為重新整理狀態
mState = REFRESH;
if (mListener != null) {
mListener.refresh();
}
smoothScrollBy( -mMoveHeight - height);
} else if (mMoveHeight > height / 2 && mState == REFRESH) {//重新整理狀態下移動到第一個view顯示小於一半會滾到到正常顯示狀態
mState = NORMAL;
smoothScrollBy( -mMoveHeight + height);
} else {
smoothScrollBy( -mMoveHeight);
}
mMoveHeight = 0;
}else{
if(mMoveHeight>mTotalHeight-getMeasuredHeight()) {//判斷上拉載入
if(mCanLoadMore) {
mState = LOAD;
if(mListener!=null){
mListener.loadMore();
}
}
if(mTotalHeight>getMeasuredHeight()) {
//滾動到最後一個view顯示的位置
smoothScrollBy(-mMoveHeight + mTotalHeight - getMeasuredHeight());
//計算滾回到最後一個view顯示位置需要移動的距離
mMoveHeight = mTotalHeight - getMeasuredHeight();
}else{
smoothScrollBy( -mMoveHeight );
mMoveHeight = 0;
}
}
}
super.dispatchTouchEvent(event);
return true;
}
return super.dispatchTouchEvent(event);
}
/**
* 處理拖動 的高度計算和回撥
*/
private void dealMove(int round, MotionEvent event) {
mMoveHeight += round;
if(mListener!=null) {
if (Math.abs(mMoveHeight) > getChildAt(0).getHeight()) {
mListener.pullDown();
}else{
mListener.pullUp();
}
}
smoothScrollBy( round);
mStartY = event.getRawY();
mStartX = event.getRawX();
}
/**
* 滑動到指定位置
* @param dy 豎直方向偏移量
*/
private void smoothScrollBy(int dy) {
mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), 0, dy);
postInvalidate();
}
最後是事件的監聽回撥 和方法呼叫:
public abstract class RefreshListener {
abstract void refresh();
void pullUp(){
}
void pullDown(){
}
void loadMore(){
}
void moveDy(int dy){
}
}
/**
* 如果含有recyclerView需要繫結之後才能滾動
*/
public void bindRecyclerView(RecyclerView recyclerView){
mRecyclerView=recyclerView;
}
/**
* 重新整理完成呼叫
*/
public void refreshComplete(){
if(mState==REFRESH) {
int height = getChildAt(0).getHeight();
mState = NORMAL;
smoothScrollBy( -mMoveHeight + height);
mMoveHeight = 0;
}
}
/**
* 上拉載入完成呼叫
* @param type 0為還有資料需要載入 1 為資料全部載入完成
*/
public void loadComplete(int type){
if(mState==LOAD) {
mState = NORMAL;
if(type==0) {
smoothScrollBy(-mMoveHeight);
mMoveHeight = 0;
}else{
//滾動到最後一個view顯示的位置
smoothScrollBy( -mMoveHeight + mTotalHeight - getMeasuredHeight());
//計算滾回到最後一個view顯示位置需要移動的距離
mMoveHeight =mTotalHeight - getMeasuredHeight();
}
}
}
/**
* 最大可彈性滾動的距離
* @param scrollDistance 距離單位px
*/
public void canScrollDistance(int scrollDistance){
mCanScrollDistance=scrollDistance;
}
/**
* 繫結重新整理狀態監聽
*/
/**
* 繫結重新整理狀態監聽
*/
public void setRefreshListener(RefreshListener listener,boolean canLoadMore){
mListener=listener;
mCanLoadMore=canLoadMore;
}
最後別忘了Scroller要想滾動需要呼叫的方法:
@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}
使用:
<com.zqb.refreshlayout.RefreshLayout
android:id="@+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="10px"
android:paddingRight="200px">
<RelativeLayout
android:id="@+id/header_content"
android:layout_width="match_parent"
android:layout_height="150px">
<LinearLayout
android:id="@+id/header_text_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/header_hint_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/header_hint_refresh_loading"
android:textSize="13sp"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/header_hint_refresh_time"
android:textSize="14sp"/>
<TextView
android:id="@+id/header_hint_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="無記錄"
android:textSize="12sp"/>
</LinearLayout>
</LinearLayout>
</RelativeLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@color/colorPrimaryDark"
android:paddingBottom="40px"
android:paddingTop="20px"
android:text="sfhsdkfh "/>
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</android.support.v7.widget.RecyclerView>
<TextView
android:text="載入中...."
android:id="@+id/load_text_view"
app:center_horizontal="true"
android:layout_width="250px"
android:layout_height="wrap_content"
android:background="@color/colorPrimaryDark"
android:paddingBottom="40px"
android:paddingTop="20px"/>
</com.zqb.refreshlayout.RefreshLayout>
mRecyclerView = findViewById(R.id.recycler_view);
mRefreshLayout = findViewById(R.id.refresh_layout);
mHeaderHintTextView = findViewById(R.id.header_hint_text);
mLoadTextView = findViewById(R.id.load_text_view);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
mRecyclerView.setLayoutManager(linearLayoutManager);
mAdapter = new MyAdapter();
mRecyclerView.setAdapter(mAdapter);
mRefreshLayout.bindRecyclerView(mRecyclerView);
mRefreshLayout.setRefreshListener(new RefreshListener() {
@Override
void refresh() {
mHeaderHintTextView.setText(R.string.header_hint_refresh_loading);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
mRefreshLayout.refreshComplete();
}
}, 2000);
}
@Override
void pullUp() {
mHeaderHintTextView.setText(R.string.header_hint_refresh_normal);
}
@Override
void pullDown() {
mHeaderHintTextView.setText(R.string.header_hint_refresh_ready);
}
@Override
void loadMore() {
super.loadMore();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if(mCount>30) {
mLoadTextView.setText("載入完畢");
mRefreshLayout.loadComplete(1);
}else {
mCount += 10;
mAdapter.notifyDataSetChanged();
mRefreshLayout.loadComplete(0);
}
}
}, 2000);
}
},false);
忘了加下拉減速效果了,之後有時間補上
GitHub上原始碼地址