先放效果图,可能跟QQ原有的实现有点差别,只能说类似
完成这个效果主要用到的就是ScrollView
的overScrollBy
这个方法主要是当你的滑到边界了然后继续向下滑动的时候触发。
/**
* Scroll the view with standard behavior for scrolling beyond the normal
* content boundaries. Views that call this method should override
* {@link #onOverScrolled(int, int, boolean, boolean)} to respond to the
* results of an over-scroll operation.
*
* Views can use this method to handle any touch or fling-based scrolling.
*
* @param deltaX Change in X in pixels
* @param deltaY Change in Y in pixels
* @param scrollX Current X scroll value in pixels before applying deltaX
* @param scrollY Current Y scroll value in pixels before applying deltaY
* @param scrollRangeX Maximum content scroll range along the X axis
* @param scrollRangeY Maximum content scroll range along the Y axis
* @param maxOverScrollX Number of pixels to overscroll by in either direction
* along the X axis.
* @param maxOverScrollY Number of pixels to overscroll by in either direction
* along the Y axis.
* @param isTouchEvent true if this scroll operation is the result of a touch event.
* @return true if scrolling was clamped to an over-scroll boundary along either
* axis, false otherwise.
*/
@Override
protected boolean overScrollBy(int deltaX, int deltaY,
int scrollX, int scrollY,
int scrollRangeX, int scrollRangeY,
int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
这里我们主要注意deltaY 变化就行,代表的是Y轴的上的变化值。 关于onOverScrolled
注释说需要重写来操作over-scroll 。 关注源代码可以知道 onOverScrolled 需要控制scrollRangeY 以及maxOverScrollY 的值才能去操作,比较麻烦。这里我们直接得到 deltaY操作就行。 如果为负代表向下滑动。
开始写代码之前我们先分析布局,解释下我的代码逻辑。
<com.example.behaviordemo.qq.QQOverScrollView
android:id="@+id/qq_scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/zoom_view"
android:layout_width="match_parent"
android:layout_height="@dimen/qq_header_height"
android:scaleType="centerCrop"
android:src="@mipmap/timg" />
<LinearLayout
android:id="@+id/content_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="wrap_content"
android:paddingLeft="30dp"
android:layout_height="@dimen/qq_header_content_height">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/profile_image"
android:layout_width="86dp"
android:layout_height="86dp"
android:layout_gravity="center_vertical"
android:layout_alignParentBottom="true"
android:src="@mipmap/lufei"
app:civ_border_width="2dp"
app:civ_border_color="#ffffff"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="300dp"
android:background="@color/aqua" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="300dp"
android:background="@color/yellow" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="300dp"
android:background="@color/blue" />
</LinearLayout>
</FrameLayout>
</com.example.behaviordemo.qq.QQOverScrollView>
如下图
左边是布局效果,右边是界面加载到模拟器效果, 可以看出我在初始化的时候,
对下面的内容部分坐了一些margin偏移.
如上图的说明,头像布局是在content_layout的布局里面,这就是为了固定头像布局让其跟随一齐滑动。这样我就可以专心处理后面的zoom_view 放大以及content_layout 布局的下滑移动问题。
zoom_view的初始大小是300dp, 下滑过程中需要增大100dp, 高度400dp
content_layout 最初位置是在margin_top100dp的位置,由于它始终要在我们看到头像的上面100dp位置,最终他的margin值要是200dp。
所以我定义了下面几个常量值,并且在init初始化的时候获取到
<dimen name="qq_header_height">300dp</dimen>
<dimen name="qq_header_max_height">400dp</dimen>
<dimen name="qq_content_margin_top">100dp</dimen>
<dimen name="qq_header_content_height">100dp</dimen>
private void init() {
Resources resources = getResources();
mInitHeaderView = resources.getDimensionPixelOffset(R.dimen.qq_header_height);
mHeaderMaxHeight = resources.getDimensionPixelOffset(R.dimen.qq_header_max_height);
mInitContentLayoutTopMargin = resources.getDimensionPixelOffset(R.dimen.qq_content_margin_top);
}
接下来就是处理滑动的逻辑
protected boolean overScrollBy(int deltaX, int deltaY,
int scrollX, int scrollY,
int scrollRangeX, int scrollRangeY,
int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
Log.d(TAG, "overScrollBy: deltaY = " + deltaY+" ; scrollY = "+scrollY);
//判断方向 小于0 代表下滑
if (mZoomView != null && deltaY < 0) {
ViewGroup.LayoutParams lp = mZoomView.getLayoutParams();
//计算出最大滑动位置 400dp - 300dp
float maxDistance = mHeaderMaxHeight - mInitHeaderView;
//最大允许的高度
if (lp.height < mHeaderMaxHeight) {
//乘0.3 为了实现阻力效果
float offestY = (float) Math.abs(deltaY * 0.3);
//滑动变化的大小加上背景图片View的高度,并重新测量绘制
lp.height = (int) (mZoomView.getHeight() + offestY);
mZoomView.requestLayout();
//计算content_layout的滑动距离
int zoomViewHeight = mZoomView.getHeight();
//当前背景图片的高度 减去 初始高度,得到是变化大小,最后得到变化的比例
float ratio = (float) (zoomViewHeight - mInitHeaderView) / maxDistance;
Log.d(TAG, "overScrollBy: zoomViewHeight = "+zoomViewHeight
+" ; mInitHeaderView = "+mInitHeaderView
+" ; ratio = "+ratio);
//每次变化这么多 changeOffestY
// 实际就是200dp大小
float totalOffsetY = maxDistance * 2 ;
FrameLayout.LayoutParams content_lp = (LayoutParams) mContentLayout.getLayoutParams();
//初始margin 加上最大偏移乘以比例, 得到的就是此时应该偏移的大小
content_lp.topMargin = (int) (mInitContentLayoutTopMargin + ratio * totalOffsetY);
mContentLayout.requestLayout();
}
}
return super.overScrollBy(deltaX, deltaY,
scrollX, scrollY,
scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
}
所有逻辑都写在了注释里面, 逻辑相对简单,所以不重复描述。
接下来一个问题,如何回弹。 这里使用的策略也很简单,我们监听用户的手指离开屏幕,我们去执行动画,回到初始位置,也就是mInitHeaderView
和mInitContentLayoutTopMargin
。
@Override
public boolean onTouchEvent(MotionEvent ev) {
handlerEvent(ev);
return super.onTouchEvent(ev);
}
private void handlerEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_UP) {
Log.d(TAG, "handlerEvent: MotionEvent.ACTION_UP");
if (mZoomView.getHeight() > mInitHeaderView) {
SpringbackAnimation mSpringbackAnimation = new SpringbackAnimation(mInitHeaderView);
mSpringbackAnimation.setInterpolator(new OvershootInterpolator());
mSpringbackAnimation.setDuration(700);
mZoomView.startAnimation(mSpringbackAnimation);
}
}
}
监听ACTION_UP 事件然后判断高度是否大于mInitHeaderView 如果大于我们就开启动画执行恢复功能。
private class SpringbackAnimation extends Animation {
private int currentHeight;
private int diffHeight;
public SpringbackAnimation(int targetHeight) {
this.currentHeight = mZoomView.getHeight();
this.diffHeight = mZoomView.getHeight() - targetHeight;
}
/**
* @param interpolatedTime 0 -1
* @param t
*/
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
Log.d(TAG, "applyTransformation: interpolatedTime = " + interpolatedTime);
mZoomView.getLayoutParams().height = (int) (currentHeight - diffHeight * interpolatedTime);
mZoomView.requestLayout();
float maxDistance = mInitHeaderView / 3;
int zoomViewHeight = mZoomView.getHeight();
float ratio = (float) (zoomViewHeight - mInitHeaderView) / maxDistance;
Log.d(TAG, "overScrollBy: zoomViewHeight = "+zoomViewHeight
+" ; mInitHeaderView = "+mInitHeaderView
+" ; ratio = "+ratio);
//每次变化这么多 changeOffestY
float totalOffsetY = maxDistance * 2 ;
FrameLayout.LayoutParams content_lp = (LayoutParams) mContentLayout.getLayoutParams();
content_lp.topMargin = (int) (mInitContentLayoutTopMargin + ratio * totalOffsetY);
mContentLayout.requestLayout();
super.applyTransformation(interpolatedTime, t);
}
}
这段代码的逻辑其实非常简单,但是需要注意的是有一个方法参数需要解释。protected void applyTransformation(float interpolatedTime, Transformation t)
其中interpolatedTime取值范围是0-1, 其实就是一个比例。 当前动画执行时间的比例。700ms的动画,时间过去了100ms 那么就是七分之一秒值。
最后贴一下完整的代码逻辑
public class QQOverScrollView extends ScrollView {
private static final String TAG = "QQHeaderScrollView";
private ImageView mZoomView;
private View mContentLayout;
private int mInitHeaderView;
private int mInitContentLayoutTopMargin;
private int mHeaderMaxHeight;
public void setViewId(@IdRes int zoomViewId, @IdRes int contentLayoutId) {
mZoomView = findViewById(zoomViewId);
mContentLayout = findViewById(contentLayoutId);
FrameLayout.LayoutParams lp = (LayoutParams) mContentLayout.getLayoutParams();
lp.topMargin = mInitContentLayoutTopMargin;
mContentLayout.requestLayout();
}
public QQOverScrollView(@NonNull Context context) {
super(context);
init();
}
public QQOverScrollView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public QQOverScrollView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
Resources resources = getResources();
mInitHeaderView = resources.getDimensionPixelOffset(R.dimen.qq_header_height);
mHeaderMaxHeight = resources.getDimensionPixelOffset(R.dimen.qq_header_max_height);
mInitContentLayoutTopMargin = resources.getDimensionPixelOffset(R.dimen.qq_content_margin_top);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
handlerEvent(ev);
return super.onTouchEvent(ev);
}
private void handlerEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_UP) {
Log.d(TAG, "handlerEvent: MotionEvent.ACTION_UP");
if (mZoomView.getHeight() > mInitHeaderView) {
SpringbackAnimation mSpringbackAnimation = new SpringbackAnimation(mInitHeaderView);
mSpringbackAnimation.setInterpolator(new OvershootInterpolator());
mSpringbackAnimation.setDuration(700);
mZoomView.startAnimation(mSpringbackAnimation);
}
}
}
@Override
protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
super.onOverScrolled(scrollX, scrollY, clampedX, clampedY);
Log.d(TAG, "onOverScrolled: scrollY = "+scrollY+" ; clampedY = "+clampedY);
}
@Override
protected boolean overScrollBy(int deltaX, int deltaY,
int scrollX, int scrollY,
int scrollRangeX, int scrollRangeY,
int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
Log.d(TAG, "overScrollBy: deltaY = " + deltaY+" ; scrollY = "+scrollY);
if (mZoomView != null && deltaY < 0) {
ViewGroup.LayoutParams lp = mZoomView.getLayoutParams();
float maxDistance = mHeaderMaxHeight - mInitHeaderView;
if (lp.height < mHeaderMaxHeight) {
float offestY = (float) Math.abs(deltaY * 0.3);
lp.height = (int) (mZoomView.getHeight() + offestY);
mZoomView.requestLayout();
int zoomViewHeight = mZoomView.getHeight();
float ratio = (float) (zoomViewHeight - mInitHeaderView) / maxDistance;
Log.d(TAG, "overScrollBy: zoomViewHeight = "+zoomViewHeight
+" ; mInitHeaderView = "+mInitHeaderView
+" ; ratio = "+ratio);
//每次变化这么多 changeOffestY
float totalOffsetY = maxDistance * 2 ;
FrameLayout.LayoutParams content_lp = (LayoutParams) mContentLayout.getLayoutParams();
content_lp.topMargin = (int) (mInitContentLayoutTopMargin + ratio * totalOffsetY);
mContentLayout.requestLayout();
}
}
return super.overScrollBy(deltaX, deltaY,
scrollX, scrollY,
scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
}
private class SpringbackAnimation extends Animation {
private int currentHeight;
private int diffHeight;
public SpringbackAnimation(int targetHeight) {
this.currentHeight = mZoomView.getHeight();
this.diffHeight = mZoomView.getHeight() - targetHeight;
}
/**
* @param interpolatedTime 0 -1
* @param t
*/
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
Log.d(TAG, "applyTransformation: interpolatedTime = " + interpolatedTime);
mZoomView.getLayoutParams().height = (int) (currentHeight - diffHeight * interpolatedTime);
mZoomView.requestLayout();
float maxDistance = mInitHeaderView / 3;
int zoomViewHeight = mZoomView.getHeight();
float ratio = (float) (zoomViewHeight - mInitHeaderView) / maxDistance;
Log.d(TAG, "overScrollBy: zoomViewHeight = "+zoomViewHeight
+" ; mInitHeaderView = "+mInitHeaderView
+" ; ratio = "+ratio);
//每次变化这么多 changeOffestY
float totalOffsetY = maxDistance * 2 ;
FrameLayout.LayoutParams content_lp = (LayoutParams) mContentLayout.getLayoutParams();
content_lp.topMargin = (int) (mInitContentLayoutTopMargin + ratio * totalOffsetY);
mContentLayout.requestLayout();
super.applyTransformation(interpolatedTime, t);
}
}
}
如果您有更好的解决办法或者比较好的实现方式,恳请指教一下。同时也欢迎大家指出不足, 谢谢。