看了一个理财类app的下拉刷新.感觉有点屌.所以特此模仿一下.
先看正版演示:
再看我这盗版演示:
aa1.gif
不得不说的是我的有点丑啊.
最上面的下拉头的问题还没有解决的很完美.有白色的边出现.
说下怎么实现的吧,
1,肯定是先创建个含有CoordinatorLayout布局的scrollingActivity了
2,然后我开始的是头像的滑动的放大和缩小.这个用了一个哥们的自定义的toolbar,代码如下:
package com.sloydev.collapsingavatartoolbar;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.support.annotation.NonNull;
import android.support.design.widget.AppBarLayout;
import android.support.v7.widget.Toolbar;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.LinearLayout;
import android.widget.TextView;
public class CollapsingAvatarToolbar extends LinearLayout implements AppBarLayout.OnOffsetChangedListener {
private View avatarView;
private TextView titleView;
private float collapsedPadding;
private float expandedPadding;
private float expandedImageSize;
private float collapsedImageSize;
private float collapsedTextSize;
private float expandedTextSize;
private boolean valuesCalculatedAlready = false;
private Toolbar toolbar;
private AppBarLayout appBarLayout;
private float collapsedHeight;
private float expandedHeight;
private float maxOffset;
private CollapseChangedListener collapseChangedListener;
public CollapsingAvatarToolbar(Context context) {
this(context, null);
init();
}
public CollapsingAvatarToolbar(Context context, AttributeSet attrs) {
super(context, attrs);
init();
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.CollapsingAvatarToolbar, 0, 0);
try {
collapsedPadding = a.getDimension(R.styleable.CollapsingAvatarToolbar_collapsedPadding, -1);
expandedPadding = a.getDimension(R.styleable.CollapsingAvatarToolbar_expandedPadding, -1);
collapsedImageSize = a.getDimension(R.styleable.CollapsingAvatarToolbar_collapsedImageSize, -1);
expandedImageSize = a.getDimension(R.styleable.CollapsingAvatarToolbar_expandedImageSize, -1);
collapsedTextSize = a.getDimension(R.styleable.CollapsingAvatarToolbar_collapsedTextSize, -1);
expandedTextSize = a.getDimension(R.styleable.CollapsingAvatarToolbar_expandedTextSize, -1);
} finally {
a.recycle();
}
final Resources resources = getResources();
if (collapsedPadding < 0) {
collapsedPadding = resources.getDimension(R.dimen.default_collapsed_padding);
}
if (expandedPadding < 0) {
expandedPadding = resources.getDimension(R.dimen.default_expanded_padding);
}
if (collapsedImageSize < 0) {
collapsedImageSize = resources.getDimension(R.dimen.default_collapsed_image_size);
}
if (expandedImageSize < 0) {
expandedImageSize = resources.getDimension(R.dimen.default_expanded_image_size);
}
if (collapsedTextSize < 0) {
collapsedTextSize = resources.getDimension(R.dimen.default_collapsed_text_size);
}
if (expandedTextSize < 0) {
expandedTextSize = resources.getDimension(R.dimen.default_expanded_text_size);
}
}
public void setCollapseChangedListener(CollapseChangedListener collapseChangedListener) {
this.collapseChangedListener = collapseChangedListener;
}
private void init() {
setOrientation(HORIZONTAL);
}
@NonNull
private AppBarLayout findParentAppBarLayout() {
ViewParent parent = this.getParent();
if (parent instanceof AppBarLayout) {
return ((AppBarLayout) parent);
} else if (parent.getParent() instanceof AppBarLayout) {
return ((AppBarLayout) parent.getParent());
} else {
throw new IllegalStateException("Must be inside an AppBarLayout"); //TODO actually, a collapsingtoolbar
}
}
protected void onAttachedToWindow() {
super.onAttachedToWindow();
findViews();
if (!isInEditMode()) {
appBarLayout.addOnOffsetChangedListener(this);
} else {
setExpandedValuesForEditMode();
}
}
private void setExpandedValuesForEditMode() {
calculateValues();
updateViews(1f, 0);
}
private void findViews() {
appBarLayout = findParentAppBarLayout();
toolbar = findSiblingToolbar();
avatarView = findAvatar();
titleView = findTitle();
}
@NonNull
private View findAvatar() {
View avatar = this.findViewById(R.id.cat_avatar);
if (avatar == null) {
throw new IllegalStateException("View with id ta_avatar not found");
}
return avatar;
}
@NonNull
private TextView findTitle() {
TextView title = (TextView) this.findViewById(R.id.cat_title);
if (title == null) {
throw new IllegalStateException("TextView with id ta_title not found");
}
return title;
}
@NonNull
private Toolbar findSiblingToolbar() {
ViewGroup parent = ((ViewGroup) this.getParent());
for (int i = 0, c = parent.getChildCount(); i < c; i++) {
View child = parent.getChildAt(i);
if (child instanceof Toolbar) {
return (Toolbar) child;
}
}
throw new IllegalStateException("No toolbar found as sibling");
}
@Override
public void onOffsetChanged(AppBarLayout appBarLayout, int offset) {
if (!valuesCalculatedAlready) {
calculateValues();
valuesCalculatedAlready = true;
}
float collapsedProgress = -offset / maxOffset;
updateViews(collapsedProgress, offset);
notifyListener(collapsedProgress);
}
private void calculateValues() {
collapsedHeight = toolbar.getHeight();
expandedHeight = appBarLayout.getHeight() - toolbar.getHeight();
maxOffset = expandedHeight;
}
private void updateViews(float collapsedProgress, int currentOffset) {
float expandedProgress = 1 - collapsedProgress;
float translation = -currentOffset + ((float) toolbar.getHeight() * expandedProgress);
float currHeight = collapsedHeight + (expandedHeight - collapsedHeight) * expandedProgress;
float currentPadding = expandedPadding + (collapsedPadding - expandedPadding) * collapsedProgress;
float currentImageSize = collapsedImageSize + (expandedImageSize - collapsedImageSize) * expandedProgress;
float currentTextSize = collapsedTextSize + (expandedTextSize - collapsedTextSize) * expandedProgress;
setContainerOffset(translation);
setContainerHeight((int) currHeight);
setPadding((int) currentPadding);
setAvatarSize((int) currentImageSize);
setTextSize(currentTextSize);
}
private void setContainerOffset(float translation) {
this.setTranslationY(translation);
}
private void setContainerHeight(int currHeight) {
this.getLayoutParams().height = currHeight;
}
private void setPadding(int currentPadding) {
this.setPadding(currentPadding, 0, 0, 0);
}
private void setTextSize(float currentTextSize) {
titleView.setTextSize(TypedValue.COMPLEX_UNIT_PX, currentTextSize);
}
private void setAvatarSize(int currentImageSize) {
avatarView.getLayoutParams().height = currentImageSize;
avatarView.getLayoutParams().width = currentImageSize;
}
private void notifyListener(float collapsedProgress) {
if (collapseChangedListener != null) {
collapseChangedListener.onCollapseChanged(collapsedProgress);
}
}
public interface CollapseChangedListener {
void onCollapseChanged(float collapsedProgress);
}
}
3,头像问题解决了之后是在滑动的时候toolbar的颜色的渐变并且还有状态栏的颜色是个toolbar的颜色的一致的渐变,这里的代码在后面会贴出
4,头像的问题解决了之后就该到了让人头疼的下拉刷新了.这个刷新是嵌套的CoordinatorLayout,在做这个效果的时候度娘都要快被我查烂了.试过各种办法.都是不了了之.当时觉得哎,好烦.后来静下心之后,有篇文章也是写下拉刷新的,用的是swipeRefreshLayout的改进,我采取了这个方法,然后自己在巨人的肩膀上在修改.代码如下:
package com.ccstest.views;
import android.content.Context;
import android.graphics.Color;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.ImageView;
import android.widget.Scroller;
import com.ccstest.R;
/**
* Created by AItsuki on 2016/6/13.
* -
*/
public class RefreshLayout extends ViewGroup {
private static final String TAG = "RefreshLayout";
private static final float DRAG_RATE = 0.5f;
private static final int INVALID_POINTER = -1;
// scroller duration
private static final int SCROLL_TO_TOP_DURATION = 800;
private static final int SCROLL_TO_REFRESH_DURATION = 250;
private static final long SHOW_COMPLETED_TIME = 500;
private View refreshHeader;
private View target;
private int currentTargetOffsetTop; // target/header偏移距离
private int lastTargetOffsetTop;
private boolean hasMeasureHeader; // 是否已经计算头部高度
private int touchSlop;
private int headerHeight; // header高度
private int totalDragDistance; // 需要下拉这个距离才进入松手刷新状态,默认和header高度一致
private int maxDragDistance;
private int activePointerId;
private boolean isTouch;
private boolean hasSendCancelEvent;
private float lastMotionX;
private float lastMotionY;
private float initDownY;
private float initDownX;
private static final int START_POSITION = 0;
private MotionEvent lastEvent;
private boolean mIsBeginDragged;
private AutoScroll autoScroll;
private State state = State.RESET;
private OnRefreshListener refreshListener;
private boolean isAutoRefresh;
// 刷新成功,显示500ms成功状态再滚动回顶部
private Runnable delayToScrollTopRunnable = new Runnable() {
@Override
public void run() {
autoScroll.scrollTo(START_POSITION, SCROLL_TO_TOP_DURATION);
}
};
private Runnable autoRefreshRunnable = new Runnable() {
@Override
public void run() {
// 标记当前是自动刷新状态,finishScroll调用时需要判断
// 在actionDown事件中重新标记为false
isAutoRefresh = true;
changeState(State.PULL);
autoScroll.scrollTo(totalDragDistance, SCROLL_TO_REFRESH_DURATION);
}
};
public RefreshLayout(Context context) {
this(context, null);
}
public RefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
autoScroll = new AutoScroll();
// 添加默认的头部,先简单的用一个ImageView代替头部
ImageView imageView = new ImageView(context);
imageView.setImageResource(R.drawable.gold);
imageView.setBackgroundColor(Color.BLACK);
setRefreshHeader(imageView);
}
/**
* 设置自定义header
*/
public void setRefreshHeader(View view) {
if (view != null && view != refreshHeader) {
removeView(refreshHeader);
// 为header添加默认的layoutParams
LayoutParams layoutParams = view.getLayoutParams();
if (layoutParams == null) {
layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, Utils.dp2px(getContext(), 80));
// layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
view.setLayoutParams(layoutParams);
}
refreshHeader = view;
addView(refreshHeader);
}
}
public void setRefreshListener(OnRefreshListener refreshListener) {
this.refreshListener = refreshListener;
}
public void refreshComplete() {
changeState(State.COMPLETE);
// if refresh completed and the target at top, change state to reset.
if (currentTargetOffsetTop == START_POSITION) {
changeState(State.RESET);
} else {
// waiting for a time to show refreshView completed state.
// at next touch event, remove this runnable
if (!isTouch) {
postDelayed(delayToScrollTopRunnable, SHOW_COMPLETED_TIME);
}
}
}
public void autoRefresh() {
autoRefresh(500);
}
/**
* 在onCreate中调用autoRefresh,此时View可能还没有初始化好,需要延长一段时间执行。
*
* @param duration 延时执行的毫秒值
*/
public void autoRefresh(long duration) {
if (state != State.RESET) {
return;
}
postDelayed(autoRefreshRunnable, duration);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (target == null) {
ensureTarget();
}
if (target == null) {
return;
}
// ----- measure target -----
// target占满整屏
target.measure(MeasureSpec.makeMeasureSpec(
getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(
getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));
// ----- measure refreshView-----
measureChild(refreshHeader, widthMeasureSpec, heightMeasureSpec);
if (!hasMeasureHeader) { // 防止header重复测量
hasMeasureHeader = true;
headerHeight = refreshHeader.getMeasuredHeight(); // header高度
totalDragDistance = headerHeight; // 需要pull这个距离才进入松手刷新状态
if (maxDragDistance == 0) { // 默认最大下拉距离为控件高度的五分之四
maxDragDistance = totalDragDistance*4/5;
}
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int width = getMeasuredWidth();
final int height = getMeasuredHeight();
if (getChildCount() == 0) {
return;
}
if (target == null) {
ensureTarget();
}
if (target == null) {
return;
}
// target铺满屏幕
final View child = target;
final int childLeft = getPaddingLeft();
final int childTop = getPaddingTop() + currentTargetOffsetTop;
final int childWidth = width - getPaddingLeft() - getPaddingRight();
final int childHeight = height - getPaddingTop() - getPaddingBottom();
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
// header放到target的上方,水平居中
int refreshViewWidth = refreshHeader.getMeasuredWidth();
refreshHeader.layout((width / 2 - refreshViewWidth / 2),
-headerHeight + currentTargetOffsetTop,
(width / 2 + refreshViewWidth / 2),
currentTargetOffsetTop);
}
/**
* 将第一个Child作为target
*/
private void ensureTarget() {
// Don't bother getting the parent height if the parent hasn't been laid
// out yet.
if (target == null) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (!child.equals(refreshHeader)) {
target = child;
break;
}
}
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (!isEnabled() || target == null) {
return super.dispatchTouchEvent(ev);
}
final int actionMasked = ev.getActionMasked(); // support Multi-touch
switch (actionMasked) {
case MotionEvent.ACTION_DOWN:
activePointerId = ev.getPointerId(0);
isAutoRefresh = false;
isTouch = true;
hasSendCancelEvent = false;
mIsBeginDragged = false;
lastTargetOffsetTop = currentTargetOffsetTop;
currentTargetOffsetTop = target.getTop();
initDownX = lastMotionX = ev.getX(0);
initDownY = lastMotionY = ev.getY(0);
autoScroll.stop();
removeCallbacks(delayToScrollTopRunnable);
removeCallbacks(autoRefreshRunnable);
super.dispatchTouchEvent(ev);
return true; // return true,否则可能接受不到move和up事件
case MotionEvent.ACTION_MOVE:
if (activePointerId == INVALID_POINTER) {
Log.e(TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
return super.dispatchTouchEvent(ev);
}
lastEvent = ev;
float x = ev.getX(MotionEventCompat.findPointerIndex(ev, activePointerId));
float y = ev.getY(MotionEventCompat.findPointerIndex(ev, activePointerId));
float yDiff = y - lastMotionY;
float offsetY = yDiff * DRAG_RATE;
lastMotionX = x;
lastMotionY = y;
if (!mIsBeginDragged && Math.abs(y - initDownY) > touchSlop) {
mIsBeginDragged = true;
}
if (mIsBeginDragged) {
boolean moveDown = offsetY > 0; // ↓
boolean canMoveDown = canChildScrollUp();
boolean moveUp = !moveDown; // ↑
boolean canMoveUp = currentTargetOffsetTop > START_POSITION;
// 判断是否拦截事件
if ((moveDown && !canMoveDown) || (moveUp && canMoveUp)) {
moveSpinner(offsetY);
return true;
}
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
isTouch = false;
if (currentTargetOffsetTop > START_POSITION) {
finishSpinner();
}
activePointerId = INVALID_POINTER;
break;
case MotionEvent.ACTION_POINTER_DOWN:
int pointerIndex = MotionEventCompat.getActionIndex(ev);
if (pointerIndex < 0) {
Log.e(TAG, "Got ACTION_POINTER_DOWN event but have an invalid action index.");
return super.dispatchTouchEvent(ev);
}
lastMotionX = ev.getX(pointerIndex);
lastMotionY = ev.getY(pointerIndex);
lastEvent = ev;
activePointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
break;
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
lastMotionY = ev.getY(ev.findPointerIndex(activePointerId));
lastMotionX = ev.getX(ev.findPointerIndex(activePointerId));
break;
}
return super.dispatchTouchEvent(ev);
}
private void moveSpinner(float diff) {
int offset = Math.round(diff);
if (offset == 0) {
return;
}
// 发送cancel事件给child
if (!hasSendCancelEvent && isTouch && currentTargetOffsetTop > START_POSITION) {
sendCancelEvent();
hasSendCancelEvent = true;
}
int targetY = Math.max(0, currentTargetOffsetTop + offset); // target不能移动到小于0的位置……
// y = x - (x/2)^2
float extraOS = targetY - totalDragDistance;
float slingshotDist = totalDragDistance;
float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, slingshotDist * 2) / slingshotDist);
float tensionPercent = (float) (tensionSlingshotPercent - Math.pow(tensionSlingshotPercent / 2, 2));
if (offset > 0) { // 下拉的时候才添加阻力
offset = (int) (offset * (1f - tensionPercent));
targetY = Math.max(0, currentTargetOffsetTop + offset);
}
// 1. 在RESET状态时,第一次下拉出现header的时候,设置状态变成PULL
if (state == State.RESET && currentTargetOffsetTop == START_POSITION && targetY > 0) {
changeState(State.PULL);
}
// 2. 在PULL或者COMPLETE状态时,header回到顶部的时候,状态变回RESET
if (currentTargetOffsetTop > START_POSITION && targetY <= START_POSITION) {
if (state == State.PULL || state == State.COMPLETE) {
changeState(State.RESET);
}
}
// 3. 如果是从底部回到顶部的过程(往上滚动),并且手指是松开状态, 并且当前是PULL状态,状态变成LOADING,这时候我们需要强制停止autoScroll
if (state == State.PULL && !isTouch && currentTargetOffsetTop > totalDragDistance && targetY <= totalDragDistance) {
autoScroll.stop();
changeState(State.LOADING);
if (refreshListener != null) {
refreshListener.onRefresh();
}
// 因为判断条件targetY <= totalDragDistance,会导致不能回到正确的刷新高度(有那么一丁点偏差),调整change
int adjustOffset = totalDragDistance - targetY;
offset += adjustOffset;
}
setTargetOffsetTopAndBottom(offset);
// 别忘了回调header的位置改变方法。
if (refreshHeader instanceof RefreshHeader) {
((RefreshHeader) refreshHeader)
.onPositionChange(currentTargetOffsetTop, lastTargetOffsetTop, totalDragDistance, isTouch, state);
}
}
private void finishSpinner() {
if (state == State.LOADING) {
if (currentTargetOffsetTop > totalDragDistance) {
autoScroll.scrollTo(totalDragDistance, SCROLL_TO_REFRESH_DURATION);
}
} else {
autoScroll.scrollTo(START_POSITION, SCROLL_TO_TOP_DURATION);
}
}
private void changeState(State state) {
this.state = state;
// Toast.makeText(getContext(), state.toString(), Toast.LENGTH_SHORT).show();
RefreshHeader refreshHeader = this.refreshHeader instanceof RefreshHeader ? ((RefreshHeader) this.refreshHeader) : null;
if (refreshHeader != null) {
switch (state) {
case RESET:
refreshHeader.reset();
break;
case PULL:
refreshHeader.pull();
break;
case LOADING:
refreshHeader.refreshing();
break;
case COMPLETE:
refreshHeader.complete();
break;
}
}
}
//add
private void setTargetOffsetTopAndBottom(int offset) {
if (offset == 0) {
return;
}
target.offsetTopAndBottom(offset);
refreshHeader.offsetTopAndBottom(offset);
lastTargetOffsetTop = currentTargetOffsetTop;
currentTargetOffsetTop = target.getTop();
// Log.e(TAG, "moveSpinner: currentTargetOffsetTop = "+ currentTargetOffsetTop);
invalidate();
}
private void sendCancelEvent() {
if (lastEvent == null) {
return;
}
MotionEvent ev = MotionEvent.obtain(lastEvent);
ev.setAction(MotionEvent.ACTION_CANCEL);
super.dispatchTouchEvent(ev);
}
private void onSecondaryPointerUp(MotionEvent ev) {
final int pointerIndex = MotionEventCompat.getActionIndex(ev);
final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
if (pointerId == activePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
lastMotionY = ev.getY(newPointerIndex);
lastMotionX = ev.getX(newPointerIndex);
activePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
}
}
public boolean canChildScrollUp() {
if (android.os.Build.VERSION.SDK_INT < 14) {
if (target instanceof AbsListView) {
final AbsListView absListView = (AbsListView) target;
return absListView.getChildCount() > 0
&& (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
.getTop() < absListView.getPaddingTop());
} else {
return ViewCompat.canScrollVertically(target, -1) || target.getScrollY() > 0;
}
} else {
return ViewCompat.canScrollVertically(target, -1);
}
}
private class AutoScroll implements Runnable {
private Scroller scroller;
private int lastY;
public AutoScroll() {
scroller = new Scroller(getContext());
}
@Override
public void run() {
boolean finished = !scroller.computeScrollOffset() || scroller.isFinished();
if (!finished) {
int currY = scroller.getCurrY();
int offset = currY - lastY;
lastY = currY;
moveSpinner(offset);
post(this);
onScrollFinish(false);
} else {
stop();
onScrollFinish(true);
}
}
public void scrollTo(int to, int duration) {
int from = currentTargetOffsetTop;
int distance = to - from;
stop();
if (distance == 0) {
return;
}
scroller.startScroll(0, 0, 0, distance, duration);
post(this);
}
private void stop() {
removeCallbacks(this);
if (!scroller.isFinished()) {
scroller.forceFinished(true);
}
lastY = 0;
}
}
/**
* 在scroll结束的时候会回调这个方法
*
* @param isForceFinish 是否是强制结束的
*/
private void onScrollFinish(boolean isForceFinish) {
if (isAutoRefresh && !isForceFinish) {
isAutoRefresh = false;
changeState(State.LOADING);
if (refreshListener != null) {
refreshListener.onRefresh();
}
finishSpinner();
}
}
public interface OnRefreshListener {
void onRefresh();
}
public enum State {
RESET, PULL, LOADING, COMPLETE
}
}
还有这个类:
package com.ccstest.views;
import android.content.Context;
import android.graphics.Color;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.ImageView;
import android.widget.Scroller;
import com.ccstest.R;
/**
* Created by AItsuki on 2016/6/13.
* -
*/
public class RefreshLayout extends ViewGroup {
private static final String TAG = "RefreshLayout";
private static final float DRAG_RATE = 0.5f;
private static final int INVALID_POINTER = -1;
// scroller duration
private static final int SCROLL_TO_TOP_DURATION = 800;
private static final int SCROLL_TO_REFRESH_DURATION = 250;
private static final long SHOW_COMPLETED_TIME = 500;
private View refreshHeader;
private View target;
private int currentTargetOffsetTop; // target/header偏移距离
private int lastTargetOffsetTop;
private boolean hasMeasureHeader; // 是否已经计算头部高度
private int touchSlop;
private int headerHeight; // header高度
private int totalDragDistance; // 需要下拉这个距离才进入松手刷新状态,默认和header高度一致
private int maxDragDistance;
private int activePointerId;
private boolean isTouch;
private boolean hasSendCancelEvent;
private float lastMotionX;
private float lastMotionY;
private float initDownY;
private float initDownX;
private static final int START_POSITION = 0;
private MotionEvent lastEvent;
private boolean mIsBeginDragged;
private AutoScroll autoScroll;
private State state = State.RESET;
private OnRefreshListener refreshListener;
private boolean isAutoRefresh;
// 刷新成功,显示500ms成功状态再滚动回顶部
private Runnable delayToScrollTopRunnable = new Runnable() {
@Override
public void run() {
autoScroll.scrollTo(START_POSITION, SCROLL_TO_TOP_DURATION);
}
};
private Runnable autoRefreshRunnable = new Runnable() {
@Override
public void run() {
// 标记当前是自动刷新状态,finishScroll调用时需要判断
// 在actionDown事件中重新标记为false
isAutoRefresh = true;
changeState(State.PULL);
autoScroll.scrollTo(totalDragDistance, SCROLL_TO_REFRESH_DURATION);
}
};
public RefreshLayout(Context context) {
this(context, null);
}
public RefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
autoScroll = new AutoScroll();
// 添加默认的头部,先简单的用一个ImageView代替头部
ImageView imageView = new ImageView(context);
imageView.setImageResource(R.drawable.gold);
imageView.setBackgroundColor(Color.BLACK);
setRefreshHeader(imageView);
}
/**
* 设置自定义header
*/
public void setRefreshHeader(View view) {
if (view != null && view != refreshHeader) {
removeView(refreshHeader);
// 为header添加默认的layoutParams
LayoutParams layoutParams = view.getLayoutParams();
if (layoutParams == null) {
layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, Utils.dp2px(getContext(), 80));
// layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
view.setLayoutParams(layoutParams);
}
refreshHeader = view;
addView(refreshHeader);
}
}
public void setRefreshListener(OnRefreshListener refreshListener) {
this.refreshListener = refreshListener;
}
public void refreshComplete() {
changeState(State.COMPLETE);
// if refresh completed and the target at top, change state to reset.
if (currentTargetOffsetTop == START_POSITION) {
changeState(State.RESET);
} else {
// waiting for a time to show refreshView completed state.
// at next touch event, remove this runnable
if (!isTouch) {
postDelayed(delayToScrollTopRunnable, SHOW_COMPLETED_TIME);
}
}
}
public void autoRefresh() {
autoRefresh(500);
}
/**
* 在onCreate中调用autoRefresh,此时View可能还没有初始化好,需要延长一段时间执行。
*
* @param duration 延时执行的毫秒值
*/
public void autoRefresh(long duration) {
if (state != State.RESET) {
return;
}
postDelayed(autoRefreshRunnable, duration);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (target == null) {
ensureTarget();
}
if (target == null) {
return;
}
// ----- measure target -----
// target占满整屏
target.measure(MeasureSpec.makeMeasureSpec(
getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(
getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));
// ----- measure refreshView-----
measureChild(refreshHeader, widthMeasureSpec, heightMeasureSpec);
if (!hasMeasureHeader) { // 防止header重复测量
hasMeasureHeader = true;
headerHeight = refreshHeader.getMeasuredHeight(); // header高度
totalDragDistance = headerHeight; // 需要pull这个距离才进入松手刷新状态
if (maxDragDistance == 0) { // 默认最大下拉距离为控件高度的五分之四
maxDragDistance = totalDragDistance*4/5;
}
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int width = getMeasuredWidth();
final int height = getMeasuredHeight();
if (getChildCount() == 0) {
return;
}
if (target == null) {
ensureTarget();
}
if (target == null) {
return;
}
// target铺满屏幕
final View child = target;
final int childLeft = getPaddingLeft();
final int childTop = getPaddingTop() + currentTargetOffsetTop;
final int childWidth = width - getPaddingLeft() - getPaddingRight();
final int childHeight = height - getPaddingTop() - getPaddingBottom();
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
// header放到target的上方,水平居中
int refreshViewWidth = refreshHeader.getMeasuredWidth();
refreshHeader.layout((width / 2 - refreshViewWidth / 2),
-headerHeight + currentTargetOffsetTop,
(width / 2 + refreshViewWidth / 2),
currentTargetOffsetTop);
}
/**
* 将第一个Child作为target
*/
private void ensureTarget() {
// Don't bother getting the parent height if the parent hasn't been laid
// out yet.
if (target == null) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (!child.equals(refreshHeader)) {
target = child;
break;
}
}
}
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (!isEnabled() || target == null) {
return super.dispatchTouchEvent(ev);
}
final int actionMasked = ev.getActionMasked(); // support Multi-touch
switch (actionMasked) {
case MotionEvent.ACTION_DOWN:
activePointerId = ev.getPointerId(0);
isAutoRefresh = false;
isTouch = true;
hasSendCancelEvent = false;
mIsBeginDragged = false;
lastTargetOffsetTop = currentTargetOffsetTop;
currentTargetOffsetTop = target.getTop();
initDownX = lastMotionX = ev.getX(0);
initDownY = lastMotionY = ev.getY(0);
autoScroll.stop();
removeCallbacks(delayToScrollTopRunnable);
removeCallbacks(autoRefreshRunnable);
super.dispatchTouchEvent(ev);
return true; // return true,否则可能接受不到move和up事件
case MotionEvent.ACTION_MOVE:
if (activePointerId == INVALID_POINTER) {
Log.e(TAG, "Got ACTION_MOVE event but don't have an active pointer id.");
return super.dispatchTouchEvent(ev);
}
lastEvent = ev;
float x = ev.getX(MotionEventCompat.findPointerIndex(ev, activePointerId));
float y = ev.getY(MotionEventCompat.findPointerIndex(ev, activePointerId));
float yDiff = y - lastMotionY;
float offsetY = yDiff * DRAG_RATE;
lastMotionX = x;
lastMotionY = y;
if (!mIsBeginDragged && Math.abs(y - initDownY) > touchSlop) {
mIsBeginDragged = true;
}
if (mIsBeginDragged) {
boolean moveDown = offsetY > 0; // ↓
boolean canMoveDown = canChildScrollUp();
boolean moveUp = !moveDown; // ↑
boolean canMoveUp = currentTargetOffsetTop > START_POSITION;
// 判断是否拦截事件
if ((moveDown && !canMoveDown) || (moveUp && canMoveUp)) {
moveSpinner(offsetY);
return true;
}
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
isTouch = false;
if (currentTargetOffsetTop > START_POSITION) {
finishSpinner();
}
activePointerId = INVALID_POINTER;
break;
case MotionEvent.ACTION_POINTER_DOWN:
int pointerIndex = MotionEventCompat.getActionIndex(ev);
if (pointerIndex < 0) {
Log.e(TAG, "Got ACTION_POINTER_DOWN event but have an invalid action index.");
return super.dispatchTouchEvent(ev);
}
lastMotionX = ev.getX(pointerIndex);
lastMotionY = ev.getY(pointerIndex);
lastEvent = ev;
activePointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
break;
case MotionEvent.ACTION_POINTER_UP:
onSecondaryPointerUp(ev);
lastMotionY = ev.getY(ev.findPointerIndex(activePointerId));
lastMotionX = ev.getX(ev.findPointerIndex(activePointerId));
break;
}
return super.dispatchTouchEvent(ev);
}
private void moveSpinner(float diff) {
int offset = Math.round(diff);
if (offset == 0) {
return;
}
// 发送cancel事件给child
if (!hasSendCancelEvent && isTouch && currentTargetOffsetTop > START_POSITION) {
sendCancelEvent();
hasSendCancelEvent = true;
}
int targetY = Math.max(0, currentTargetOffsetTop + offset); // target不能移动到小于0的位置……
// y = x - (x/2)^2
float extraOS = targetY - totalDragDistance;
float slingshotDist = totalDragDistance;
float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, slingshotDist * 2) / slingshotDist);
float tensionPercent = (float) (tensionSlingshotPercent - Math.pow(tensionSlingshotPercent / 2, 2));
if (offset > 0) { // 下拉的时候才添加阻力
offset = (int) (offset * (1f - tensionPercent));
targetY = Math.max(0, currentTargetOffsetTop + offset);
}
// 1. 在RESET状态时,第一次下拉出现header的时候,设置状态变成PULL
if (state == State.RESET && currentTargetOffsetTop == START_POSITION && targetY > 0) {
changeState(State.PULL);
}
// 2. 在PULL或者COMPLETE状态时,header回到顶部的时候,状态变回RESET
if (currentTargetOffsetTop > START_POSITION && targetY <= START_POSITION) {
if (state == State.PULL || state == State.COMPLETE) {
changeState(State.RESET);
}
}
// 3. 如果是从底部回到顶部的过程(往上滚动),并且手指是松开状态, 并且当前是PULL状态,状态变成LOADING,这时候我们需要强制停止autoScroll
if (state == State.PULL && !isTouch && currentTargetOffsetTop > totalDragDistance && targetY <= totalDragDistance) {
autoScroll.stop();
changeState(State.LOADING);
if (refreshListener != null) {
refreshListener.onRefresh();
}
// 因为判断条件targetY <= totalDragDistance,会导致不能回到正确的刷新高度(有那么一丁点偏差),调整change
int adjustOffset = totalDragDistance - targetY;
offset += adjustOffset;
}
setTargetOffsetTopAndBottom(offset);
// 别忘了回调header的位置改变方法。
if (refreshHeader instanceof RefreshHeader) {
((RefreshHeader) refreshHeader)
.onPositionChange(currentTargetOffsetTop, lastTargetOffsetTop, totalDragDistance, isTouch, state);
}
}
private void finishSpinner() {
if (state == State.LOADING) {
if (currentTargetOffsetTop > totalDragDistance) {
autoScroll.scrollTo(totalDragDistance, SCROLL_TO_REFRESH_DURATION);
}
} else {
autoScroll.scrollTo(START_POSITION, SCROLL_TO_TOP_DURATION);
}
}
private void changeState(State state) {
this.state = state;
// Toast.makeText(getContext(), state.toString(), Toast.LENGTH_SHORT).show();
RefreshHeader refreshHeader = this.refreshHeader instanceof RefreshHeader ? ((RefreshHeader) this.refreshHeader) : null;
if (refreshHeader != null) {
switch (state) {
case RESET:
refreshHeader.reset();
break;
case PULL:
refreshHeader.pull();
break;
case LOADING:
refreshHeader.refreshing();
break;
case COMPLETE:
refreshHeader.complete();
break;
}
}
}
//add
private void setTargetOffsetTopAndBottom(int offset) {
if (offset == 0) {
return;
}
target.offsetTopAndBottom(offset);
refreshHeader.offsetTopAndBottom(offset);
lastTargetOffsetTop = currentTargetOffsetTop;
currentTargetOffsetTop = target.getTop();
// Log.e(TAG, "moveSpinner: currentTargetOffsetTop = "+ currentTargetOffsetTop);
invalidate();
}
private void sendCancelEvent() {
if (lastEvent == null) {
return;
}
MotionEvent ev = MotionEvent.obtain(lastEvent);
ev.setAction(MotionEvent.ACTION_CANCEL);
super.dispatchTouchEvent(ev);
}
private void onSecondaryPointerUp(MotionEvent ev) {
final int pointerIndex = MotionEventCompat.getActionIndex(ev);
final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex);
if (pointerId == activePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
lastMotionY = ev.getY(newPointerIndex);
lastMotionX = ev.getX(newPointerIndex);
activePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex);
}
}
public boolean canChildScrollUp() {
if (android.os.Build.VERSION.SDK_INT < 14) {
if (target instanceof AbsListView) {
final AbsListView absListView = (AbsListView) target;
return absListView.getChildCount() > 0
&& (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0)
.getTop() < absListView.getPaddingTop());
} else {
return ViewCompat.canScrollVertically(target, -1) || target.getScrollY() > 0;
}
} else {
return ViewCompat.canScrollVertically(target, -1);
}
}
private class AutoScroll implements Runnable {
private Scroller scroller;
private int lastY;
public AutoScroll() {
scroller = new Scroller(getContext());
}
@Override
public void run() {
boolean finished = !scroller.computeScrollOffset() || scroller.isFinished();
if (!finished) {
int currY = scroller.getCurrY();
int offset = currY - lastY;
lastY = currY;
moveSpinner(offset);
post(this);
onScrollFinish(false);
} else {
stop();
onScrollFinish(true);
}
}
public void scrollTo(int to, int duration) {
int from = currentTargetOffsetTop;
int distance = to - from;
stop();
if (distance == 0) {
return;
}
scroller.startScroll(0, 0, 0, distance, duration);
post(this);
}
private void stop() {
removeCallbacks(this);
if (!scroller.isFinished()) {
scroller.forceFinished(true);
}
lastY = 0;
}
}
/**
* 在scroll结束的时候会回调这个方法
*
* @param isForceFinish 是否是强制结束的
*/
private void onScrollFinish(boolean isForceFinish) {
if (isAutoRefresh && !isForceFinish) {
isAutoRefresh = false;
changeState(State.LOADING);
if (refreshListener != null) {
refreshListener.onRefresh();
}
finishSpinner();
}
}
public interface OnRefreshListener {
void onRefresh();
}
public enum State {
RESET, PULL, LOADING, COMPLETE
}
}
基本下拉OK了.现在在巨人肩膀上我来模仿这个正版的头部和波浪的下拉刷新
先得来个波浪:
代码如下:
package com.ccstest.views;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DrawFilter;
import android.graphics.Paint;
import android.graphics.PaintFlagsDrawFilter;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.View;
public class WaveView3 extends View {
private Path mAbovePath,mBelowWavePath;
private Paint mAboveWavePaint,mBelowWavePaint;
private DrawFilter mDrawFilter;
private float φ;
private OnWaveAnimationListener mWaveAnimationListener;
public WaveView3(Context context, AttributeSet attrs) {
super(context, attrs);
//初始化路径
mAbovePath = new Path();
mBelowWavePath = new Path();
//初始化上面的画笔
mAboveWavePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mAboveWavePaint.setAntiAlias(true);
mAboveWavePaint.setStyle(Paint.Style.FILL);
mAboveWavePaint.setColor(Color.WHITE);
//初始化下面的画笔
mBelowWavePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mBelowWavePaint.setAntiAlias(true);
mBelowWavePaint.setStyle(Paint.Style.FILL);
mBelowWavePaint.setColor(Color.WHITE);
mBelowWavePaint.setAlpha(80);
//画布抗锯齿
mDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
}
@Override
protected void onDraw(Canvas canvas) {
canvas.setDrawFilter(mDrawFilter);
mAbovePath.reset();//清除画笔中的内容
mBelowWavePath.reset();
φ-=0.1f;
float y,y2;
double ω = 6*Math.PI / getWidth();
mAbovePath.moveTo(getLeft(),getBottom());
mBelowWavePath.moveTo(getLeft(),getBottom());
for (float x = 0; x <= getWidth(); x += 20) {
/**
* y=Asin(ωx+φ)+k
* A—振幅越大,波形在y轴上最大与最小值的差值越大
* ω—角速度, 控制正弦周期(单位角度内震动的次数)
* φ—初相,反映在坐标系上则为图像的左右移动。这里通过不断改变φ,达到波浪移动效果
* k—偏距,反映在坐标系上则为图像的上移或下移。
*/
y = (float) (8 * Math.cos(ω * x + φ) +8);
y2 = (float) (8 * Math.sin(ω * x + φ));
mAbovePath.lineTo(x, y);
mBelowWavePath.lineTo(x, y2);
//回调 把y坐标的值传出去(在activity里面接收让小机器人随波浪一起摇摆)
// mWaveAnimationListener.OnWaveAnimation(y);
}
mAbovePath.lineTo(getRight(),getBottom());
mBelowWavePath.lineTo(getRight(),getBottom());
canvas.drawPath(mAbovePath,mAboveWavePaint);
canvas.drawPath(mBelowWavePath,mBelowWavePaint);
postInvalidateDelayed(10);
}
public void setOnWaveAnimationListener(OnWaveAnimationListener l){
this.mWaveAnimationListener = l;
}
public interface OnWaveAnimationListener{
void OnWaveAnimation(float y);
}
}
然后呢,基本上就OK了.
接下来把布局和activity中的代码贴下:
布局:
<?xml version="1.0" encoding="utf-8"?>
<com.ccstest.views.RefreshLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/sw"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:addStatesFromChildren="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
>
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context="com.ccstest.activity.ScrollingActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/app_bar_height"
android:fitsSystemWindows="true"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.design.widget.CollapsingToolbarLayout
android:id="@+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/wallet_bg4"
android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed"
>
<LinearLayout
android:id="@+id/llllll"
android:layout_width="match_parent"
android:layout_height="80dp"
android:orientation="horizontal"
android:visibility="gone"
>
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginLeft="30dp"
android:src="@drawable/player_btn_share"
/>
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"/>
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginRight="30dp"
android:src="@drawable/player_btn_share"
/>
</LinearLayout>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left|center"
android:layout_marginLeft="30dp"
android:src="@drawable/icon_heart"
/>
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_gravity="right|center"
android:layout_marginRight="30dp"
android:src="@drawable/icon_heart"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center|bottom"
android:layout_marginBottom="10dp"
android:text="注册/登录"
android:textSize="16sp"
/>
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="@style/AppTheme.PopupOverlay">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
</RelativeLayout>
</android.support.v7.widget.Toolbar>
<com.sloydev.collapsingavatartoolbar.CollapsingAvatarToolbar
android:id="@+id/avatartoolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:collapsedImageSize="30dp"
app:collapsedPadding="2dp"
app:expandedImageSize="60dp"
app:expandedPadding="2dp"
>
<LinearLayout
android:id="@+id/ll"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="horizontal">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@id/cat_avatar"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_marginBottom="12dp"
android:src="@drawable/icon"
app:border_color="#fff"
app:border_width="2dp"
/>
<TextView
android:id="@id/cat_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
/>
</LinearLayout>
</com.sloydev.collapsingavatartoolbar.CollapsingAvatarToolbar>
<com.ccstest.views.WaveView3
android:id="@+id/wave_view"
android:layout_width="match_parent"
android:layout_height="10dp"
android:layout_gravity="bottom"
android:visibility="gone"
/>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<include layout="@layout/content_scrolling"/>
</android.support.design.widget.CoordinatorLayout>
</com.ccstest.views.RefreshLayout>
哎,简书说我文章太长了,需要源码的同志们点赞后留下你们的邮箱,
传送门
网友评论
tryohang@yahoo.com