我们先看效果图:
这是仿苹果app的一种下拉head拉伸的效果,这种弹性伸缩能给用户一种良好的体验,因此我们在各大主流app上也能看的到这种效果,而PullZoomView是一款在安卓能实现上述效果并且能实现视差效果的开源框架。今天让我们来对此框架一探究竟!
先来看看项目结构:
此项目包括了:IPollToZoom,一个接口类定义了一些供实例对象调用的方法; PullToZoomBase,一个基类定义了一些公共方法;然后
PullToZoomListViewEx 与 PullToZoomScrollViewEx 分别是继承base所派生的两个子类。
那么我们就先来看看PullToZoomBase基类:
package com.yinbaner.view;
import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.Gravity;
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.widget.LinearLayout;
import com.yinbaner.wifibox.R;
public abstract class PullToZoomBase<T extends View> extends LinearLayout implements IPullToZoom<T> {
private static final float FRICTION = 2.0f;
protected T mRootView;
protected View mHeaderView;//头部View
protected View mZoomView;//缩放拉伸View
protected int mScreenHeight;
protected int mScreenWidth;
private boolean isZoomEnabled = true;
private boolean isParallax = true;
private boolean isZooming = false;
private boolean isHideHeader = false;
private int mTouchSlop;
private boolean mIsBeingDragged = false;
private float mLastMotionY;
private float mLastMotionX;
private float mInitialMotionY;
private OnPullZoomListener onPullZoomListener;
public PullToZoomBase(Context context) {
this(context, null);
}
public PullToZoomBase(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
private void init(Context context, AttributeSet attrs) {
setGravity(Gravity.CENTER);
ViewConfiguration config = ViewConfiguration.get(context);
mTouchSlop = config.getScaledTouchSlop();//滑动标准
DisplayMetrics localDisplayMetrics = new DisplayMetrics();
((Activity) getContext()).getWindowManager().getDefaultDisplay().getMetrics(localDisplayMetrics);
mScreenHeight = localDisplayMetrics.heightPixels; //屏幕像素h
mScreenWidth = localDisplayMetrics.widthPixels; //屏幕像素w
// Refreshable View
// By passing the attrs, we can add ListView/GridView params via XML
mRootView = createRootView(context, attrs); //抽象方法,子类实现返回子类创建的mRootView
if (attrs != null) {
LayoutInflater mLayoutInflater = LayoutInflater.from(getContext());
//初始化状态View
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.PullToZoomView);
int zoomViewResId = a.getResourceId(R.styleable.PullToZoomView_zoomView, 0);//获得xml中定义的zommView ID
if (zoomViewResId > 0) {
mZoomView = mLayoutInflater.inflate(zoomViewResId, null, false);
}
int headerViewResId = a.getResourceId(R.styleable.PullToZoomView_headerView, 0);//获得xml中定义的headerView ID
if (headerViewResId > 0) {
mHeaderView = mLayoutInflater.inflate(headerViewResId, null, false);
}
isParallax = a.getBoolean(R.styleable.PullToZoomView_isHeaderParallax, true);//获得xml中定义的是否支持视差
// Let the derivative classes have a go at handling attributes, then
// recycle them...
handleStyledAttributes(a);//传入TypedArray,让子类获取到xml中定义的contentView ID
a.recycle();
}
addView(mRootView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
}
public void setOnPullZoomListener(OnPullZoomListener onPullZoomListener) {
this.onPullZoomListener = onPullZoomListener;
}
@Override
public T getPullRootView() {
return mRootView;
}
@Override
public View getZoomView() {
return mZoomView;
}
@Override
public View getHeaderView() {
return mHeaderView;
}
@Override
public boolean isPullToZoomEnabled() {
return isZoomEnabled;
}
@Override
public boolean isZooming() {
return isZooming;
}
@Override
public boolean isParallax() {
return isParallax;
}
@Override
public boolean isHideHeader() {
return isHideHeader;
}
public void setZoomEnabled(boolean isZoomEnabled) {
this.isZoomEnabled = isZoomEnabled;
}
public void setParallax(boolean isParallax) {
this.isParallax = isParallax;
}
public void setHideHeader(boolean isHideHeader) {//header显示才能Zoom
this.isHideHeader = isHideHeader;
}
private VelocityTracker mVelocityTracker;
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (!isPullToZoomEnabled() || isHideHeader()) {
return false;
}
final int action = event.getAction();
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mIsBeingDragged = false;
return false;
}
if (action != MotionEvent.ACTION_DOWN && mIsBeingDragged) {
return true;
}
switch (action) {
case MotionEvent.ACTION_MOVE: {
if (isReadyForPullStart()) {//如果处于顶部
final float y = event.getY(), x = event.getX();
final float diff, oppositeDiff, absDiff;
// We need to use the correct values, based on scroll
// direction
diff = y - mLastMotionY;
oppositeDiff = x - mLastMotionX;
absDiff = Math.abs(diff);
//如果是滑动,并且是垂直滑动
if (absDiff > mTouchSlop && absDiff > Math.abs(oppositeDiff)) {
if (diff >= 1f && isReadyForPullStart()) {//如果处于顶部
mLastMotionY = y;
mLastMotionX = x;
mIsBeingDragged = true;
}
}
}
break;
}
case MotionEvent.ACTION_DOWN: {
if (isReadyForPullStart()) {//如果处于顶部
mLastMotionY = mInitialMotionY = event.getY();
mIsBeingDragged = false;
}
break;
}
}
return mIsBeingDragged;
}
@Override
public boolean onTouchEvent( MotionEvent event) {
if (!isPullToZoomEnabled() || isHideHeader()) {
return false;
}
if (event.getAction() == MotionEvent.ACTION_DOWN && event.getEdgeFlags() != 0) {//getEdgeFlags触到边缘
return false;
}
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE: {
if (mIsBeingDragged) {
mLastMotionY = event.getY();
mLastMotionX = event.getX();
pullEvent();//下拉事件处理
isZooming = true;
return true;
}
break;
}
case MotionEvent.ACTION_DOWN: {
if (isReadyForPullStart()) {
mLastMotionY = mInitialMotionY = event.getY();
return true;
}
break;
}
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP: {
if (mIsBeingDragged) {
mIsBeingDragged = false;
// If we're already refreshing, just scroll back to the top
if (isZooming()) {// ↑
//我发现这个注释是经典的下拉刷新框架PulToRefresh里面的,由此可以看出此框架有借鉴了PulToRefresh的思想
smoothScrollToTop();//调用子类的缩回逻辑
if (onPullZoomListener != null) {
onPullZoomListener.onPullZoomEnd();
}
isZooming = false;
return true;
}
return true;
}
break;
}
}
return false;
}
private void pullEvent() {
final int newScrollValue;
final float initialMotionValue, lastMotionValue;
initialMotionValue = mInitialMotionY; //开始按下的Y坐标
lastMotionValue = mLastMotionY;//滑动移动的Y坐标
//当放大head时候,随时的计算手指滑动的 y距离/2
newScrollValue = Math.round(Math.min(initialMotionValue - lastMotionValue, 0) / FRICTION);
//调用子类放大逻辑,传入差值
pullHeaderToZoom(newScrollValue);
if (onPullZoomListener != null) {
onPullZoomListener.onPullZooming(newScrollValue);
}
}
protected abstract void pullHeaderToZoom(int newScrollValue);
public abstract void setHeaderView(View headerView);
public abstract void setZoomView(View zoomView);
protected abstract T createRootView(Context context, AttributeSet attrs);
protected abstract void smoothScrollToTop();
protected abstract boolean isReadyForPullStart();
public interface OnPullZoomListener {
void onPullZooming(int newScrollValue);
void onPullZoomEnd();
}
}
必要的地方都做了注释,大体上是实现了接口,重写了一些外部操作的方法,定义了一些抽象方法,用于从子类获取到返回的结果,并且根据一些条件做了事件的拦截和处理和通过MotionEvent计算出处于顶部的时候下拉的距离,传给子类做head高度拉伸处理。
再来看看 PullToZoomScrollViewEx
package com.yinbaner.view;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Interpolator;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import com.yinbaner.wifibox.R;
public class PullToZoomScrollViewEx extends PullToZoomBase<ScrollView> {
private static final String TAG = PullToZoomScrollViewEx.class.getSimpleName();
private boolean isCustomHeaderHeight = false;
private FrameLayout mHeaderContainer;
private LinearLayout mRootContainer;
private View mContentView;
private int mHeaderHeight;
private ScalingRunnable mScalingRunnable;
private static final Interpolator sInterpolator = new Interpolator() {
@Override
public float getInterpolation(float paramAnonymousFloat) {
float f = paramAnonymousFloat - 1.0F;
return 1.0F + f * (f * (f * (f * f)));
}
};
public PullToZoomScrollViewEx(Context context) {
this(context, null);
}
public PullToZoomScrollViewEx(Context context, AttributeSet attrs) {
super(context, attrs);
mScalingRunnable = new ScalingRunnable();
((InternalScrollView) mRootView).setOnScrollViewChangedListener(new OnScrollViewChangedListener() {
@Override
public void onInternalScrollChanged(int left, int top, int oldLeft, int oldTop) {
if (isPullToZoomEnabled() && isParallax()) {//视差效果实现代码块,即是滑动scrollView的时候同时移动mHeaderContainer
Log.d(TAG, "onScrollChanged --> getScrollY() = " + mRootView.getScrollY());
float f = mHeaderHeight - mHeaderContainer.getBottom() + mRootView.getScrollY();
Log.d(TAG, "onScrollChanged --> f = " + f);
if(osc != null)
osc.OnScrollChangeListener(f);//若实现了监听,则回调
if ((f > 0.0F) && (f < mHeaderHeight)) {
int i = (int) (0.65D * f);
mHeaderContainer.scrollTo(0, -i);//两者移动距离之商为0.65,所以产生视差效果
} else if (mHeaderContainer.getScrollY() != 0) {
mHeaderContainer.scrollTo(0, 0);//移动mHeaderContainer
}
}
}
@Override
public void OnScrollChangeListener(float f) {
// TODO Auto-generated method stub
}
});
}
@Override
protected void pullHeaderToZoom(int newScrollValue) {//放大逻辑
Log.d(TAG, "pullHeaderToZoom --> newScrollValue = " + newScrollValue);
Log.d(TAG, "pullHeaderToZoom --> mHeaderHeight = " + mHeaderHeight);
if (mScalingRunnable != null && !mScalingRunnable.isFinished()) {
mScalingRunnable.abortAnimation();
}
//,根据传入的差值动态改变高度
ViewGroup.LayoutParams localLayoutParams = mHeaderContainer.getLayoutParams(); //获取到mHeaderContainer的LayoutParams
localLayoutParams.height = Math.abs(newScrollValue) + mHeaderHeight;//改变高度
mHeaderContainer.setLayoutParams(localLayoutParams);//设置新的LayoutParams
if (isCustomHeaderHeight) {
ViewGroup.LayoutParams zoomLayoutParams = mZoomView.getLayoutParams();
zoomLayoutParams.height = Math.abs(newScrollValue) + mHeaderHeight;
mZoomView.setLayoutParams(zoomLayoutParams);
}
}
/**
* 是否显示headerView
*
* @param isHideHeader true: show false: hide
*/
@Override
public void setHideHeader(boolean isHideHeader) {
if (isHideHeader != isHideHeader() && mHeaderContainer != null) {
super.setHideHeader(isHideHeader);
if (isHideHeader) {
mHeaderContainer.setVisibility(GONE);
} else {
mHeaderContainer.setVisibility(VISIBLE);
}
}
}
/**
* 动态设置headerView
*
* @param headerView
*/
@Override
public void setHeaderView(View headerView) {
if (headerView != null) {
mHeaderView = headerView;
updateHeaderView();
}
}
/**
* 动态设置zoomView
*
* @param zoomView
*/
@Override
public void setZoomView(View zoomView) {
if (zoomView != null) {
mZoomView = zoomView;
updateHeaderView();
}
}
/**
* 更新HeaderView
*/
private void updateHeaderView() {
if (mHeaderContainer != null) {
mHeaderContainer.removeAllViews();
if (mZoomView != null) {
mHeaderContainer.addView(mZoomView);
}
if (mHeaderView != null) {
mHeaderContainer.addView(mHeaderView);
}
}
}
/**
* 动态设置contentView
*
* @param contentView
*/
public void setScrollContentView(View contentView) {
if (contentView != null) {
if (mContentView != null) {
mRootContainer.removeView(mContentView);
}
mContentView = contentView;
mRootContainer.addView(mContentView);
}
}
/**
* 实现父类的抽象方法,用于返回scrollView给父类
*
*/
@Override
protected ScrollView createRootView(Context context, AttributeSet attrs) {
ScrollView scrollView = new InternalScrollView(context, attrs);
scrollView.setId(R.id.scrollview);
return scrollView;
}
/**
* 伸缩之后的回弹
*/
@Override
protected void smoothScrollToTop() {
Log.d(TAG, "smoothScrollToTop --> ");
mScalingRunnable.startAnimation(200L);
}
/**
* 是否除于顶部
*/
@Override
protected boolean isReadyForPullStart() {
return mRootView.getScrollY() == 0;
}
@Override //初始化view
public void handleStyledAttributes(TypedArray a) {
mRootContainer = new LinearLayout(getContext());
mRootContainer.setOrientation(LinearLayout.VERTICAL);
mHeaderContainer = new FrameLayout(getContext());
if (mZoomView != null) {
mHeaderContainer.addView(mZoomView);
}
if (mHeaderView != null) {
mHeaderContainer.addView(mHeaderView);
}
int contentViewResId = a.getResourceId(R.styleable.PullToZoomView_contentView, 0);
if (contentViewResId > 0) {
LayoutInflater mLayoutInflater = LayoutInflater.from(getContext());
mContentView = mLayoutInflater.inflate(contentViewResId, null, false);
}
mRootContainer.addView(mHeaderContainer);
if (mContentView != null) {
mRootContainer.addView(mContentView);
}
mRootContainer.setClipChildren(false);//允许子View超出范围
mHeaderContainer.setClipChildren(false);
mRootView.addView(mRootContainer);
}
/**
* 设置HeaderView高度
*
* @param width
* @param height
*/
public void setHeaderViewSize(int width, int height) {
if (mHeaderContainer != null) {
Object localObject = mHeaderContainer.getLayoutParams();
if (localObject == null) {
localObject = new ViewGroup.LayoutParams(width, height);
}
((ViewGroup.LayoutParams) localObject).width = width;
((ViewGroup.LayoutParams) localObject).height = height;
mHeaderContainer.setLayoutParams((ViewGroup.LayoutParams) localObject);
mHeaderHeight = height;
isCustomHeaderHeight = true;
}
}
/**
* 设置HeaderView LayoutParams
*
* @param layoutParams LayoutParams
*/
public void setHeaderLayoutParams(LinearLayout.LayoutParams layoutParams) {
if (mHeaderContainer != null) {
mHeaderContainer.setLayoutParams(layoutParams);
mHeaderHeight = layoutParams.height;
isCustomHeaderHeight = true;
}
}
@Override
protected void onLayout(boolean paramBoolean, int paramInt1, int paramInt2,
int paramInt3, int paramInt4) {
super.onLayout(paramBoolean, paramInt1, paramInt2, paramInt3, paramInt4);
Log.d(TAG, "onLayout --> ");
if (mHeaderHeight == 0 && mZoomView != null) {
mHeaderHeight = mHeaderContainer.getHeight();
}
}
/**
* 通过调用 startAnimation
* 来调用 ScalingRunnable的run()方法里面的回弹逻辑
*/
class ScalingRunnable implements Runnable {
protected long mDuration;
protected boolean mIsFinished = true;
protected float mScale;
protected long mStartTime;
ScalingRunnable() {
}
public void abortAnimation() {
mIsFinished = true;
}
public boolean isFinished() {
return mIsFinished;
}
public void run() {
if (mZoomView != null) {
if ((!mIsFinished) && (mScale > 1.0D)) {
float f1 = ((float) SystemClock.currentThreadTimeMillis() - (float) mStartTime) / (float) mDuration;
float f2 = mScale - (mScale - 1.0F) * PullToZoomScrollViewEx.sInterpolator.getInterpolation(f1);
Log.d(TAG, "ScalingRunnable --> f2 = " + f2);
if (f2 > 1.0F) {
ViewGroup.LayoutParams localLayoutParams = mHeaderContainer.getLayoutParams();
localLayoutParams.height = ((int) (f2 * mHeaderHeight));
mHeaderContainer.setLayoutParams(localLayoutParams);//动态改变高度
if (isCustomHeaderHeight) {
ViewGroup.LayoutParams zoomLayoutParams = mZoomView.getLayoutParams();
zoomLayoutParams.height = ((int) (f2 * mHeaderHeight));
mZoomView.setLayoutParams(zoomLayoutParams);
}
post(this);
return;
}
mIsFinished = true;
}
}
}
public void startAnimation(long paramLong) {//传入duration
if (mZoomView != null) {
mStartTime = SystemClock.currentThreadTimeMillis();
mDuration = paramLong;
mScale = ((float) (mHeaderContainer.getBottom()) / mHeaderHeight);
mIsFinished = false;
post(this);
}
}
}
//重写了ScrollView并且添加了滑动监听方法
protected class InternalScrollView extends ScrollView {
private OnScrollViewChangedListener onScrollViewChangedListener;
public InternalScrollView(Context context) {
this(context, null);
}
public InternalScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setOnScrollViewChangedListener(OnScrollViewChangedListener onScrollViewChangedListener) {
this.onScrollViewChangedListener = onScrollViewChangedListener;
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
super.onScrollChanged(l, t, oldl, oldt);
if (onScrollViewChangedListener != null) {
onScrollViewChangedListener.onInternalScrollChanged(l, t, oldl, oldt);
}
}
}
//定义的滑动的回调接口
private OnScrollViewChangedListener osc;
public void setOnScrollViewChangedListener(OnScrollViewChangedListener osc){
this.osc = osc;
}
public interface OnScrollViewChangedListener {
public void onInternalScrollChanged(int left, int top, int oldLeft, int oldTop);
public void OnScrollChangeListener(float f);
}
}
主要结论
①根据传入的滑动距离/2的值加上原mHeaderContainer的高度设置到它的layoutparams中来达到放大的效果;
②视差效果是两个view同方向上移动速度的差别所产生的效果.
由于篇幅原因,这里只贴出对 PullToZoom ScrollViewEx 的主要注释,理解了PullToZoomScrollViewEx , PullToZoomListViewEx也是同理的。
想要看完整代码及demo,请链至github→ https://github.com/Frank-Zhu/PullZoomView