效果图
改进
- 解决先上拉再下拉时的问题
- 加入箭头动画
原理
解决先上拉再下拉时的问题关键在于如何在 custom view 的滑动和 scroll view 的滑动之间切换。从上向下滑动的时候,应该可以从 custom view 的滑动无缝过渡到 scroll view 的滑动。如最后一张图所示。这点豆瓣一刻没有做到。我通过插入一个 touch down event实现。体验更佳。箭头动画相对简单,RotateAnimation。
关键源码
public class PtrLinearLayout extends LinearLayout {
private final static float SCROLL_RATIO = 0.35f;
private static final long DURATION = 200;
private static final long ROTATE_ANIM_DURATION = 180;
enum STATE {CAN_TURN_PAGE, CAN_NOT_TURN_PAGE};
private STATE state = STATE.CAN_NOT_TURN_PAGE;
private View contentView;
private View footerView;
private float lastY = -1;
private ScrollView scrollView;
private int footerViewHeight;
private TextView footerTextView;
private Animation rotateUpAnim;
private Animation rotateDownAnim;
private ImageView arrowImageView;
public PtrLinearLayout(Context context) {
super(context);
init();
}
public PtrLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public PtrLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
rotateUpAnim = new RotateAnimation(0.0f, -180.0f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
0.5f);
rotateUpAnim.setDuration(ROTATE_ANIM_DURATION);
rotateUpAnim.setFillAfter(true);
rotateDownAnim = new RotateAnimation(-180.0f, 0.0f,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF,
0.5f);
rotateDownAnim.setDuration(ROTATE_ANIM_DURATION);
rotateDownAnim.setFillAfter(true);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
contentView = getChildAt(0);
footerView = getChildAt(1);
footerView.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
footerViewHeight = footerView.getMeasuredHeight();
LayoutParams layoutParams = (LayoutParams) footerView.getLayoutParams();
layoutParams.height = footerViewHeight;
footerView.setLayoutParams(layoutParams);
scrollView = (ScrollView) contentView;
arrowImageView = (ImageView) findViewById(R.id.iv_ptr_arrow);
footerTextView = (TextView) footerView.findViewById(R.id.tv_ptr_operation_hint);
}
/**
* should get touch event from here, otherwise you can not scroll out the footerView closely after scroll the scrollview
*
* @param event
* @return
*/
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
if (lastY == -1){
lastY = event.getY();
}
float dy = event.getY() - lastY;
lastY = event.getY();
boolean linearLayoutScrolling = false;
float scrollViewContinueScrollDy = 0;
if (getScrollY() != 0 || (isScrollViewReachBottom() && dy < 0)) {
linearLayoutScrolling = true;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
//never let getScrollY < 0
int calibrateDScrollY = (int) (-dy * SCROLL_RATIO);
if ((getScrollY() - dy * SCROLL_RATIO) < 0){
calibrateDScrollY = -getScrollY();
scrollViewContinueScrollDy = dy * SCROLL_RATIO - getScrollY();
}
scrollBy(0, calibrateDScrollY);
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (getScrollY() >= footerViewHeight) {
if (footerListener != null) {
footerListener.onTrigger();
}
} else {
// scrollTo(0, 0);
smoothScrollTo(0, 0);
}
break;
}
if (getScrollY() >= footerViewHeight && state == STATE.CAN_NOT_TURN_PAGE) {
footerTextView.setText("松开翻页");
arrowImageView.startAnimation(rotateDownAnim);
state = STATE.CAN_TURN_PAGE;
} else if (getScrollY() < footerViewHeight && state == STATE.CAN_TURN_PAGE) {
footerTextView.setText("上拉翻页");
arrowImageView.startAnimation(rotateUpAnim);
state = STATE.CAN_NOT_TURN_PAGE;
}
}
if (getScrollY() == 0) {
if (linearLayoutScrolling){
MotionEvent downEvent = MotionEvent.obtain(event);
downEvent.setAction(MotionEvent.ACTION_DOWN | event.getActionIndex() << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
downEvent.setLocation(event.getX(), event.getY()-scrollViewContinueScrollDy);
super.dispatchTouchEvent(downEvent);
}
return super.dispatchTouchEvent(event);
} else {
// if (getScrollY() < 0) {
// scrollTo(0, 0);
// }
MotionEvent cancelEvent = MotionEvent.obtain(event);
cancelEvent.setAction(MotionEvent.ACTION_CANCEL|(event.getActionIndex()<< MotionEvent.ACTION_POINTER_INDEX_SHIFT));
super.dispatchTouchEvent(cancelEvent);
return true;
}
}
private void smoothScrollTo(int toScrollX, int toScrollY) {
Animation scrollYAnimation = new ScrollYAnimation(getScrollY(), toScrollY);
scrollYAnimation.setInterpolator(new AccelerateDecelerateInterpolator());
scrollYAnimation.setDuration(DURATION);
startAnimation(scrollYAnimation);
}
private class ScrollYAnimation extends Animation{
private int fromScrollY, toScrollY, scrollYDelta;
private ScrollYAnimation(int fromScrollY, int toScrollY) {
this.fromScrollY = fromScrollY;
this.toScrollY = toScrollY;
scrollYDelta = toScrollY - fromScrollY;
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
if (interpolatedTime == 1){
scrollTo(0, toScrollY);
return;
}
scrollTo(0, (int) (fromScrollY + scrollYDelta*interpolatedTime));
}
}
private boolean isScrollViewReachBottom() {
return scrollView.getScrollY() >= scrollView.getChildAt(0).getMeasuredHeight() - scrollView.getHeight();
}
private FooterListener footerListener;
public void setFooterListener(FooterListener footerListener) {
this.footerListener = footerListener;
}
public static interface FooterListener {
public void onTrigger();
}
}