/**
* Created by AItsuki on 2016/6/13.
*
*/
public enum State {
RESET, PULL, LOADING, COMPLETE
}
/**
* Created by AItsuki on 2016/6/13.
*
*/
public interface RefreshHeader {
/**
* 松手,头部隐藏后会回调这个方法
*/
void reset();
/**
* 下拉出头部的一瞬间调用
*/
void pull();
/**
* 正在刷新的时候调用
*/
void refreshing();
/**
* 头部滚动的时候持续调用
* @param currentPos target当前偏移高度
* @param lastPos target上一次的偏移高度
* @param refreshPos 可以松手刷新的高度
* @param isTouch 手指是否按下状态(通过scroll自动滚动时需要判断)
* @param state 当前状态
*/
void onPositionChange(float currentPos, float lastPos, float refreshPos, boolean isTouch, State state);
/**
* 刷新成功的时候调用
*/
void complete();
}
package com.aitsuki.custompulltorefresh;
import android.content.Context;
import android.graphics.Color;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.ImageView;
/**
* Created by AItsuki on 2016/6/13.
* -
*/
public class RefreshLayout extends ViewGroup {
private View refreshHeader;
private View target;
private int currentTargetOffsetTop; // target偏移距离
private boolean hasMeasureHeader; // 是否已经计算头部高度
private int touchSlop;
private int headerHeight; // header高度
private int totalDragDistance; // 需要下拉这个距离才进入松手刷新状态,默认和header高度一致
public RefreshLayout(Context context) {
this(context, null);
}
public RefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
// 添加默认的头部,先简单的用一个ImageView代替头部
ImageView imageView = new ImageView(context);
imageView.setImageResource(R.drawable.one_piece);
imageView.setBackgroundColor(Color.BLACK);
setRefreshHeader(imageView);
}
/**
* 设置自定义header
*/
public void setRefreshHeader(View view) {
if (view != null && view != refreshHeader) {
removeView(refreshHeader);
// 为header添加默认的layoutParams
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
if (layoutParams == null) {
layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
view.setLayoutParams(layoutParams);
}
refreshHeader = view;
addView(refreshHeader);
}
}
@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这个距离才进入松手刷新状态
}
}
@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;
}
// onLayout执行的时候,要让target和header加上偏移距离(初始0),因为有可能在滚动它们的时候,child请求重新布局,从而导致target和header瞬间回到原位。
// 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;
}
}
}
}
}
<?xml version="1.0" encoding="utf-8"?> <FrameLayout 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" tools:context="com.aitsuki.custompulltorefresh.MainActivity"> <com.aitsuki.custompulltorefresh.RefreshLayout android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:text="Target" android:textSize="30sp" android:gravity="center" android:background="#FFDAB9" /> </com.aitsuki.custompulltorefresh.RefreshLayout> </FrameLayout>
@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:
Log.e(TAG, "ACTION_DOWN");
activePointerId = ev.getPointerId(0);
isTouch = true; // 手指是否按下
hasSendCancelEvent = false;
mIsBeginDragged = false; // 是否开始下拉
lastTargetOffsetTop = currentTargetOffsetTop; // 上一次target的偏移高度
currentTargetOffsetTop = target.getTop(); // 当前target偏移高度
initDownX = lastMotionX = ev.getX(0); // 手指按下时的坐标
initDownY = lastMotionY = ev.getY(0);
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; // 最后一次move事件
float x = ev.getX(MotionEventCompat.findPointerIndex(ev,activePointerId));
float y = ev.getY(MotionEventCompat.findPointerIndex(ev,activePointerId));
float xDiff = x - lastMotionX;
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;
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);
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 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 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的位置……
offset = targetY - currentTargetOffsetTop;
setTargetOffsetTopAndBottom(offset);
}
private void setTargetOffsetTopAndBottom(int offset) {
if (offset == 0) {
return;
}
target.offsetTopAndBottom(offset);
refreshHeader.offsetTopAndBottom(offset);
lastTargetOffsetTop = currentTargetOffsetTop;
currentTargetOffsetTop = target.getTop();
invalidate();
}
private void sendCancelEvent() {
if (lastEvent == null) {
return;
}
MotionEvent ev = MotionEvent.obtain(lastEvent);
ev.setAction(MotionEvent.ACTION_CANCEL);
super.dispatchTouchEvent(ev);
}
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); // 调用此方法移动header和target
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) {
}
case MotionEvent.ACTION_DOWN: //... autoScroll.stop(); //... break
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
//...
if(currentTargetOffsetTop > START_POSITION) {
autoScroll.scrollTo(START_POSITION, SCROLL_TO_TOP_DURATION);
}
//...
private void changeState(State state) {
this.state = state;
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;
}
}
}
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);
}
}
}
// 刷新成功,显示500ms成功状态再滚动回顶部,这个runnalbe需要在ActionDown事件中Remove
private Runnable delayToScrollTopRunnable = new Runnable() {
@Override
public void run() {
autoScroll.scrollTo(START_POSITION, SCROLL_TO_TOP_DURATION);
}
};
// 定义一个侦听器
public interface OnRefreshListener {
void onRefresh();
}
// 提供外部设置方法
public void setRefreshListener(OnRefreshListener refreshListener) {
this.refreshListener = refreshListener;
}
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的位置……
offset = targetY - currentTargetOffsetTop;
// 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);
}
}
final RefreshLayout refreshLayout = (RefreshLayout) findViewById(R.id.refreshLayout);
if (refreshLayout != null) {
// 刷新状态的回调
refreshLayout.setRefreshListener(new RefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
// 延迟3秒后刷新成功
refreshLayout.postDelayed(new Runnable() {
@Override
public void run() {
refreshLayout.refreshComplete();
}
}, 3000);
}
});
}
<rotate xmlns:android="http://schemas.android.com/apk/res/android" android:duration="150" android:fillAfter="true" android:fromDegrees="-180" android:pivotX="50%" android:pivotY="50%" android:repeatCount="0" android:toDegrees="0" />
<rotate xmlns:android="http://schemas.android.com/apk/res/android" android:duration="150" android:fillAfter="true" android:fromDegrees="0" android:pivotX="50%" android:pivotY="50%" android:toDegrees="180" />
<rotate xmlns:android="http://schemas.android.com/apk/res/android" android:duration="150" android:fillAfter="true" android:fromDegrees="180" android:interpolator="@android:anim/linear_interpolator" android:pivotX="50%" android:pivotY="50%" android:repeatCount="0" android:toDegrees="0" />
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.FrameLayout;
import android.widget.TextView;
/**
* Created by AItsuki on 2016/6/15.
*
*/
public class QQRefreshHeader extends FrameLayout implements RefreshHeader {
private Animation rotate_up;
private Animation rotate_down;
private Animation rotate_infinite;
private TextView textView;
private View arrowIcon;
private View successIcon;
private View loadingIcon;
public QQRefreshHeader(Context context) {
this(context, null);
}
public QQRefreshHeader(Context context, AttributeSet attrs) {
super(context, attrs);
// 初始化动画
rotate_up = AnimationUtils.loadAnimation(context , R.anim.rotate_up);
rotate_down = AnimationUtils.loadAnimation(context , R.anim.rotate_down);
rotate_infinite = AnimationUtils.loadAnimation(context , R.anim.rotate_infinite);
inflate(context, R.layout.header_qq, this);
textView = (TextView) findViewById(R.id.text);
arrowIcon = findViewById(R.id.arrowIcon);
successIcon = findViewById(R.id.successIcon);
loadingIcon = findViewById(R.id.loadingIcon);
}
@Override
public void reset() {
textView.setText(getResources().getText(R.string.qq_header_reset));
successIcon.setVisibility(INVISIBLE);
arrowIcon.setVisibility(VISIBLE);
arrowIcon.clearAnimation();
loadingIcon.setVisibility(INVISIBLE);
loadingIcon.clearAnimation();
}
@Override
public void pull() {
}
@Override
public void refreshing() {
arrowIcon.setVisibility(INVISIBLE);
loadingIcon.setVisibility(VISIBLE);
textView.setText(getResources().getText(R.string.qq_header_refreshing));
arrowIcon.clearAnimation();
loadingIcon.startAnimation(rotate_infinite);
}
@Override
public void onPositionChange(float currentPos, float lastPos, float refreshPos, boolean isTouch, State state) {
// 往上拉
if (currentPos < refreshPos && lastPos >= refreshPos) {
if (isTouch && state == State.PULL) {
textView.setText(getResources().getText(R.string.qq_header_pull));
arrowIcon.clearAnimation();
arrowIcon.startAnimation(rotate_down);
}
// 往下拉
} else if (currentPos > refreshPos && lastPos <= refreshPos) {
if (isTouch && state == State.PULL) {
textView.setText(getResources().getText(R.string.qq_header_pull_over));
arrowIcon.clearAnimation();
arrowIcon.startAnimation(rotate_up);
}
}
}
@Override
public void complete() {
loadingIcon.setVisibility(INVISIBLE);
loadingIcon.clearAnimation();
successIcon.setVisibility(VISIBLE);
textView.setText(getResources().getText(R.string.qq_header_completed));
}
}
public void autoRefresh() {
autoRefresh(500);
}
/**
* 在onCreate中调用autoRefresh,此时View可能还没有初始化好,需要延长一段时间执行。
*
* @param duration 延时执行的毫秒值
*/
public void autoRefresh(long duration) {
if (state != State.RESET) {
return;
}
postDelayed(autoRefreshRunnable, duration);
}
// 自动刷新,需要等View初始化完毕才调用,否则头部不会滚动出现
private Runnable autoRefreshRunnable = new Runnable() {
@Override
public void run() {
// 标记当前是自动刷新状态,finishScroll调用时需要判断
// 在actionDown事件中重新标记为false
isAutoRefresh = true;
changeState(State.PULL);
autoScroll.scrollTo(totalDragDistance, SCROLL_TO_REFRESH_DURATION);
}
};
/**
* 滚动结束回调
*
* @param isForceFinish 是否强制停止
*/
private void onScrollFinish(boolean isForceFinish) {
if (isAutoRefresh && !isForceFinish) {
isAutoRefresh = false;
changeState(State.LOADING);
if (refreshListener != null) {
refreshListener.onRefresh();
}
finishSpinner();
}
}
// 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);
}
机械节能产品生产企业官网模板...
大气智能家居家具装修装饰类企业通用网站模板...
礼品公司网站模板
宽屏简约大气婚纱摄影影楼模板...
蓝白WAP手机综合医院类整站源码(独立后台)...苏ICP备2024110244号-2 苏公网安备32050702011978号 增值电信业务经营许可证编号:苏B2-20251499 | Copyright 2018 - 2025 源码网商城 (www.ymwmall.com) 版权所有