为了重新了解一下自定义ViewGroup,自己实现了一个下拉刷新view,冲突的解决Recyclerview滚动到底部和顶部的处理全部放在了父view 中,滚动实现使用的是Scroller,所以使整个控件还有类似ios的弹性效果,代码很简单,使用也很简单,刷新的头布局和脚布局都可以在布局文件中直接添加,处理。
其中内容也可以不是Recyclerview,直接写在布局里就行,布局效果类似LinearLayout 但只能是竖直方向堆叠。
初始化布局等代码:
private Scroller mScroller;
private RecyclerView mRecyclerView;
private RefreshListener mListener;
private int mTotalHeight; //子view加在一起总高度
private HashMap mViewMarginTop; //所有子view marginTop距离
private HashMap 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资源文件,仅仅只取了几个处理了一下:
<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">
"@+id/header_content"
android:layout_width="match_parent"
android:layout_height="150px">
"@+id/header_text_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:orientation="vertical">
"@+id/header_hint_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/header_hint_refresh_loading"
android:textSize="13sp"/>
"wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp">
"wrap_content"
android:layout_height="wrap_content"
android:text="@string/header_hint_refresh_time"
android:textSize="14sp"/>
"@+id/header_hint_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="无记录"
android:textSize="12sp"/>
"wrap_content"
android:layout_height="wrap_content"
android:background="@color/colorPrimaryDark"
android:paddingBottom="40px"
android:paddingTop="20px"
android:text="sfhsdkfh "/>
.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content">
.support.v7.widget.RecyclerView>
"加载中...."
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上源码地址