實現拉繩效果(類似UC瀏覽器首頁效果)
前段時間,由於需求需要實現一個拉繩效果的首頁,所以自己是實現了這樣一個功能,需要的同學可以參考一下
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.content.Context;
import android.os.Build;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.AnticipateInterpolator;
import android.view.animation.BounceInterpolator;
import android.view.animation.CycleInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.OvershootInterpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.Scroller;
/**
* Created by Jesse on 2016/9/19.
*/
public class RopeView extends FrameLayout {
private static final String DEBUG_TAG = "scrollview";
private static final int ANIM_DURATION = 250;
private static final int MAX_LINE_EXTEND = 200;
private float mLastTouchY;
private float downY;
private int lineHeight;
/**
* 手指相對於剛按下位置的距離
*/
private float distanceY = 0;
private VelocityTracker mVelocityTracker = null;
/**
* 用於完成滾動操作的實例
*/
private Scroller mScroller;
private boolean mIsScrolling = false;
private float mVelocityY, mVelocityX;
private int mTouchSlop;
private int mMinFlingVelocity;
private int mMaxFlingVelocity;
private View headView, contentView, bottomView;
private ImageView ropeIV;
private View line, ropeRL;
private Status status = Status.top;
private RopeListener mListener;
private View ropeContentRL;
public interface RopeListener {
/**
* 狀態改變
*
* @param status
*/
public void onStausChange(Status status);
}
public enum Status {
top, bottom
}
public RopeView(Context context) {
super(context);
init();
}
public RopeView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public RopeView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
prepareView();
mScroller = new Scroller(getContext(), new AccelerateInterpolator(2));
ViewConfiguration vc = ViewConfiguration.get(getContext());
mTouchSlop = vc.getScaledTouchSlop();
mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity();
initStatus();
}
private OnClickListener ropeClickListener = new OnClickListener() {
@Override
public void onClick(View v) {
switchView();
}
};
private void switchView() {
switch (status) {
case top:
mScroller.startScroll(ropeContentRL.getScrollX(), ropeContentRL.getScrollY(), 0, -ropeContentRL.getScrollY(), ANIM_DURATION); //向下滾
break;
case bottom:
mScroller.startScroll(ropeContentRL.getScrollX(), ropeContentRL.getScrollY(), 0, getScrollHeight() - ropeContentRL.getScrollY(), ANIM_DURATION);
break;
}
invalidate();
}
private void prepareView() {
LayoutInflater inflater = LayoutInflater.from(getContext());
inflater.inflate(R.layout.view_scroll, this, true);
headView = findViewById(R.id.head);
contentView = findViewById(R.id.content);
bottomView = findViewById(R.id.bottom);
ropeIV = (ImageView) findViewById(R.id.iv_rope);
ropeContentRL = findViewById(R.id.rl_rope_content);
line = findViewById(R.id.line);
ropeRL = findViewById(R.id.rl_rope);
ropeIV.setOnClickListener(ropeClickListener);
}
/**
* 設置背景的Fragment
* @param manager
* @param fragment
*/
public void setBackgroundFragment(FragmentManager manager,Fragment fragment) {
if(fragment == null) return;
FragmentTransaction ft = manager.beginTransaction();
ft.replace(R.id.fl_background_container,fragment);
ft.commitAllowingStateLoss();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
/*
* This method JUST determines whether we want to intercept the motion.
* If we return true, onTouchEvent will be called and we do the actual
* scrolling there.
*/
final int action = event.getActionMasked();
// Always handle the case of the touch gesture being complete.
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
// Release the scroll.
mIsScrolling = false;
return false; // Do not intercept touch event, let the child handle it
}
switch (action) {
case MotionEvent.ACTION_DOWN:
downY = event.getY();
mScroller.abortAnimation();
mLastTouchY = event.getY();
initTracker(event);
bringToFront();
break;
case MotionEvent.ACTION_MOVE: {
if (mIsScrolling) {
// We're currently scrolling, so yes, intercept the
// touch event!
return true;
}
// If the user has dragged her finger horizontally more than
// the touch slop, start the scroll
float yDiff = Math.abs(event.getY() - downY);
// Touch slop should be calculated using ViewConfiguration
// constants.
if (yDiff > mTouchSlop) {
// Start scrolling!
mIsScrolling = true;
return true;
}
break;
}
}
// In general, we don't want to intercept touch events. They should be
// handled by the child view.
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
boolean handled = false;
int action = event.getActionMasked();
switch (action) {
case (MotionEvent.ACTION_DOWN):
Log.d(DEBUG_TAG, "onTouchEvent Action was DOWN");
mScroller.abortAnimation();
mLastTouchY = event.getY();
initTracker(event);
handled = true;
break;
case (MotionEvent.ACTION_MOVE):
Log.d(DEBUG_TAG, "onTouchEvent Action was MOVE");
calculateVelocity(event);
float y = event.getY();
float dy = y - mLastTouchY;
distanceY = y - downY;
if (dy < 0 || ropeContentRL.getScrollY() > 0) {
ropeContentRL.scrollBy(0, -(int) dy);
}
if (ropeContentRL.getScrollY() < 0) {
ropeContentRL.scrollTo(ropeContentRL.getScrollX(), 0); //防止滑動到上面
}
extendLine();
invalidate(); //必須刷新
mLastTouchY = y;
if (distanceY > mTouchSlop) handled = true;
break;
case (MotionEvent.ACTION_UP):
case (MotionEvent.ACTION_CANCEL):
Log.d(DEBUG_TAG, "onTouchEvent Action was ACTION_UP or ACTION_CANCEL");
if (mLastTouchY < downY) { //向上滑
if (enableFling() || (Math.abs(ropeContentRL.getScrollY()) > (getScrollHeight() / 3))) {
mScroller.startScroll(ropeContentRL.getScrollX(), ropeContentRL.getScrollY(), 0, getScrollHeight() - ropeContentRL.getScrollY(), ANIM_DURATION); //向上滾
} else {
mScroller.startScroll(ropeContentRL.getScrollX(), ropeContentRL.getScrollY(), 0, -ropeContentRL.getScrollY(), ANIM_DURATION); //向下滾
}
} else { //向下滑
if (enableFling() || ((getScrollHeight() - Math.abs(ropeContentRL.getScrollY())) > (getScrollHeight() / 3))) {
mScroller.startScroll(ropeContentRL.getScrollX(), ropeContentRL.getScrollY(), 0, -ropeContentRL.getScrollY(), ANIM_DURATION); //向下滾
} else {
mScroller.startScroll(ropeContentRL.getScrollX(), ropeContentRL.getScrollY(), 0, getScrollHeight() - ropeContentRL.getScrollY(), ANIM_DURATION); //向上滾
}
}
invalidate(); //必須刷新
releaseTracker();
resetLine();
mVelocityX = 0;
mVelocityY = 0;
distanceY = 0;
break;
case (MotionEvent.ACTION_OUTSIDE):
Log.d(DEBUG_TAG, "Movement occurred outside bounds " +
"of current screen element");
break;
}
super.onTouchEvent(event);
return handled;
}
/**
* 伸長下垂的那根線
*/
private void extendLine() {
if (distanceY < 0) return;
double x = distanceY / getScrollHeight(); //範圍是[0-1]
double factor = -(Math.pow(x, 2)) + 2 * x;
int extendHeight = (int) (factor * MAX_LINE_EXTEND);
Log.e(DEBUG_TAG, "extendHeight = " + extendHeight);
ViewGroup.LayoutParams params = line.getLayoutParams();
params.height = lineHeight + extendHeight;
line.setLayoutParams(params);
line.invalidate();
}
private void resetLine() {
if (distanceY <= 0) return;
line.setPivotY(0);
line.setPivotX(line.getWidth() / 2);
ObjectAnimator animator = ObjectAnimator.ofInt(line, "custom", line.getHeight(), lineHeight);
animator.setInterpolator(new BounceInterpolator());
animator.setDuration(ANIM_DURATION * 2);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int val = (int) animation.getAnimatedValue();
ViewGroup.LayoutParams params = line.getLayoutParams();
params.height = val;
line.setLayoutParams(params);
line.invalidate();
}
});
animator.start();
}
private void startRopeAnim() {
AnimatorSet animatorSet = new AnimatorSet();
ObjectAnimator animator1 = ObjectAnimator.ofFloat(ropeRL, View.TRANSLATION_Y, -bottomView.getHeight());
animator1.setInterpolator(new DecelerateInterpolator(3));
animator1.setDuration(ANIM_DURATION + 100);
ObjectAnimator animator2 = ObjectAnimator.ofFloat(ropeRL, View.TRANSLATION_Y, 0);
animator2.setInterpolator(new BounceInterpolator());
animator2.setDuration(ANIM_DURATION + 100);
ObjectAnimator animator3 = ObjectAnimator.ofFloat(ropeIV, View.ROTATION_Y, 90);
animator3.setInterpolator(new CycleInterpolator(1));
animator3.setDuration(ANIM_DURATION * 2);
ObjectAnimator animator4 = ObjectAnimator.ofFloat(ropeRL, View.ROTATION, -10);
animator4.setInterpolator(new CycleInterpolator(2));
animator4.setDuration(ANIM_DURATION * 4);
ropeRL.setPivotX(ropeRL.getWidth() / 2);
ropeRL.setPivotY(0);
animatorSet.play(animator2).after(animator1);
animatorSet.play(animator3).after(animator2);
animatorSet.play(animator3).with(animator4);
animatorSet.start();
}
private void startRopeInitAnim() {
AnimatorSet animatorSet = new AnimatorSet();
ObjectAnimator animator1 = ObjectAnimator.ofFloat(ropeRL, View.ALPHA, 0.2f, 1.0f);
animator1.setInterpolator(new AccelerateInterpolator(2));
animator1.setDuration(ANIM_DURATION * 2);
ObjectAnimator animator2 = ObjectAnimator.ofFloat(ropeRL, View.ROTATION, -10);
animator2.setInterpolator(new CycleInterpolator(2));
animator2.setDuration(ANIM_DURATION * 4);
ropeRL.setPivotX(ropeRL.getWidth() / 2);
ropeRL.setPivotY(0);
animatorSet.play(animator2).after(animator1);
animatorSet.start();
}
/**
* 計算手機滑動的速度
*
* @param event
*/
private void calculateVelocity(MotionEvent event) {
if (mVelocityTracker == null) return;
mVelocityTracker.addMovement(event);
// When you want to determine the velocity, call
// computeCurrentVelocity(). Then call getXVelocity()
// and getYVelocity() to retrieve the velocity for each pointer ID.
mVelocityTracker.computeCurrentVelocity(1000, mMaxFlingVelocity);
mVelocityX = mVelocityTracker.getXVelocity();
mVelocityY = mVelocityTracker.getYVelocity();
}
/**
* 返回滾動的最大高度
*
* @return
*/
private int getScrollHeight() {
return headView.getHeight() + contentView.getHeight();
}
private boolean enableFling() {
return Math.abs(mVelocityY) > Math.abs(mVelocityX) && Math.abs(mVelocityY) > mMinFlingVelocity;
}
private void initTracker(MotionEvent event) {
if (mVelocityTracker == null) {
// Retrieve a new VelocityTracker object to watch the velocity of a motion.
mVelocityTracker = VelocityTracker.obtain();
} else {
// Reset the velocity tracker back to its initial state.
mVelocityTracker.clear();
}
// Add a user's movement to the tracker.
mVelocityTracker.addMovement(event);
}
private void releaseTracker() {
if (null != mVelocityTracker) {
mVelocityTracker.clear();
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
@Override
public void computeScroll() {
// 如果返回true,表示動畫還沒有結束
// 因爲前面startScroll,所以只有在startScroll完成時 纔會爲false
if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
Log.e(DEBUG_TAG, mScroller.getCurrX() + "======" + mScroller.getCurrY());
// 產生了動畫效果,根據當前值 每次滾動一點
ropeContentRL.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
//此時同樣也需要刷新View ,否則效果可能有誤差
invalidate();
return;
}
completeScroll();
}
private void completeScroll() {
if (Math.abs(ropeContentRL.getScrollY()) == getScrollHeight()) {
if (status == Status.bottom) { //從底部滾動高頂部播放動畫
startRopeAnim();
Log.e(DEBUG_TAG, "滾動到top結束");
status = Status.top;
if (mListener != null) {
mListener.onStausChange(Status.top);
}
}
}
if (Math.abs(ropeContentRL.getScrollY()) == 0) {
if (status == Status.top) {
Log.e(DEBUG_TAG, "滾動到bottom結束");
status = Status.bottom;
if (mListener != null) {
mListener.onStausChange(Status.bottom);
}
}
}
invalidate();
}
/**
* 初始化剛開始頁面狀態
*/
private void initStatus() {
this.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (status == Status.top) {
mScroller.startScroll(ropeContentRL.getScrollX(), ropeContentRL.getScrollY(), 0, getScrollHeight() - ropeContentRL.getScrollY(), 0); //立刻向上滾
invalidate();
startRopeInitAnim();
}
lineHeight = line.getHeight();
if (hasJellyBean()) {
RopeView.this.getViewTreeObserver()
.removeOnGlobalLayoutListener(this);
} else {
RopeView.this.getViewTreeObserver()
.removeGlobalOnLayoutListener(this);
}
}
});
}
private boolean hasJellyBean() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN;
}
@Override
protected void onDetachedFromWindow() {
// To be on the safe side, abort the scroller
if ((mScroller != null) && !mScroller.isFinished()) {
mScroller.abortAnimation();
}
super.onDetachedFromWindow();
}
public void setOnRopeListener(RopeListener mListener) {
this.mListener = mListener;
}
public void changeRopeIcon(final int resid) {
AnimatorSet animatorSet = new AnimatorSet();
ObjectAnimator animator1 = ObjectAnimator.ofFloat(ropeIV, View.ROTATION_Y, 180);
animator1.setInterpolator(new DecelerateInterpolator());
animator1.setDuration(ANIM_DURATION);
animator1.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
ropeIV.setImageResource(resid);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
ObjectAnimator animator2 = ObjectAnimator.ofFloat(ropeRL, View.ROTATION, -5);
animator2.setInterpolator(new CycleInterpolator(1));
animator2.setDuration(ANIM_DURATION * 2);
ropeRL.setPivotX(ropeRL.getWidth() / 2);
ropeRL.setPivotY(0);
animatorSet.play(animator2).with(animator1);
animatorSet.start();
}
}
佈局頁面如下:
<RelativeLayout 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.example.cw80jd2.myapplication.MainActivity">
<com.example.cw80jd2.myapplication.RopeView
android:id="@+id/ropeView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</RelativeLayout>
效果如下: