(点击上方公众号,可快速关注)
来源:伯乐在线专栏作者 - joe
链接:http://android.jobbole.com/85137/
点击 → 了解如何加入专栏作者
这是山寨UC浏览器的下拉刷新效果的的结尾篇了,到这里,基本是实现了UC浏览器首页的效果了!还没有看之前的小伙伴记得出门左转先看看哟(Android 自定义View UC下拉刷新效果(一)、Android 自定义View UC下拉刷新效果(二))。期间也有不小的改动,主要集中在那个小圆球拖拽时的绘制方式上,可以看到,最后的圆球效果比之前的顺畅漂亮了很多!!
pull.png
back.png
loading.png
[GIF图超过2MB,微信无法显示]
经过前面的两篇文章,分别从小球动画和下拉刷新两个方面介绍了相关的内容,最后还剩首页显示过渡列表展示的内容了!效果说明:
1、向上滑动,背景和tab有个渐变效果
2、向下滑动,有一个放大和圆弧出现
功能拆分
1、展开关闭top默认值
因为这里有两种状态,一种是展开的,一种是首页的关闭状态,展开的默认top是TabLayout的对应高度加上自身的top值,而关闭时,默认top值是上面的CurveView的高度加上自身的top值。
2、实现拖拽滑动效果
首先想到的就是ViewDragHelper,使用它来控制相关的拖拽。
3、拖拽背景渐变效果
这个就是设置拖拽过程中相关的回调。另外就是在首页的状态,ViewPager是没法左右滑动的。
4、绘制下拉的弧度
这个就得使用到drawPath()绘制贝塞尔曲线了。
相关对象介绍
父布局是一个CurveLayout,里面包含三个对象:
// child views & helpers
private View sheet;//target
private ViewDragHelper sheetDragHelper;
private ViewOffsetHelper sheetOffsetHelper;
sheet就是我们的拖拽目标View,ViewDragHelper拖拽辅助类,写好对应的事件处理和Callback就可以实现拖拽功能了!这里不详细介绍。ViewOffsetHelper,对于它的介绍,可以看看下面的截图:
ViewOffsetHelper.png
因为我们这里只涉及上下的移动,所以介绍以下主要方法:
//构造方法
public ViewOffsetHelper(View view) {
mView = view;
}
//onlayoutChange时调用
public void onViewLayout() {
// Grab the intended top & left
mLayoutTop = mView.getTop();
mLayoutLeft = mView.getLeft();
// And offset it as needed
updateOffsets();
}
//View位置改变时调用该方法
public boolean setTopAndBottomOffset(int absoluteOffset) {
if (mOffsetTop != absoluteOffset) {
mOffsetTop = absoluteOffset;
updateOffsets();
return true;
}
return false;
}
//同步
public void resyncOffsets() {
mOffsetTop = mView.getTop() - mLayoutTop;
mOffsetLeft = mView.getLeft() - mLayoutLeft;
}
//更新值
private void updateOffsets() {
ViewCompat.offsetTopAndBottom(mView, mOffsetTop - (mView.getTop() - mLayoutTop));
ViewCompat.offsetLeftAndRight(mView, mOffsetLeft - (mView.getLeft() - mLayoutLeft));
}
展开、关闭的默认top值
@Override
public void addView(View child, int index, ViewGroup.LayoutParams params) {
if (sheet != null) {
throw new UnsupportedOperationException("CurveLayout must only have 1 child view");
}
sheet = child;
sheetOffsetHelper = new ViewOffsetHelper(sheet);
sheet.addOnLayoutChangeListener(sheetLayout);
// force the sheet contents to be gravity bottom. This ain't a top sheet.
((LayoutParams) params).gravity = Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL;
super.addView(child, index, params);
}
在addView()的方法中我们确定对应的Target,然后为其设置一个OnLayoutChangeListener。
//设置默认的dismissTop值
public void setDismissOffset(int dismissOffset) {
this.dismissOffset = currentTop + dismissOffset;
}
//设置默认的expandTop值
public void setExpandTopOffset(int tabOffset) {
if (this.expandTopOffset != tabOffset) {
this.expandTopOffset = tabOffset;
sheetExpandedTop = currentTop + expandTopOffset;
}
}
接下来看看OnLayoutChangeListener里面的相关逻辑:
private final OnLayoutChangeListener sheetLayout = new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
sheetExpandedTop = top + expandTopOffset;
sheetBottom = bottom;
currentTop = top;
sheetOffsetHelper.onViewLayout();
// modal bottom sheet content should not initially be taller than the 16:9 keyline
if (!initialHeightChecked) {
applySheetInitialHeightOffset(false, -1);
initialHeightChecked = true;
} else if (!hasInteractedWithSheet
&& (oldBottom - oldTop) != (bottom - top)) { /* sheet height changed */
/* if the sheet content's height changes before the user has interacted with it
then consider this still in the 'initial' state and apply the height constraint,
but in this case, animate to it */
applySheetInitialHeightOffset(true, oldTop - sheetExpandedTop);
}
Log.e(TAG, "onLayoutChange: 布局变化了!!" + sheet.getTop());
}
};
初始化sheetExpandedTop,currentTop等字段,并且调用上面提到的onViewLayout(),同步ViewOffsetHelper的值。
拖拽滑动实现
ViewDragHelper就不多说了,Android自带的辅助类,添加一个Callback,然后处理相关回调方法就可以了!
判断是否拦截处理事件:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
currentX = ev.getRawX();
Log.e(TAG, "BottomSheet onInterceptTouchEvent: " + currentX);
if (isExpanded()) {
sheetDragHelper.cancel();
return false;
}
hasInteractedWithSheet = true;
final int action = MotionEventCompat.getActionMasked(ev);
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
sheetDragHelper.cancel();
return false;
}
return isDraggableViewUnder((int) ev.getX(), (int) ev.getY())
&& (sheetDragHelper.shouldInterceptTouchEvent(ev));
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
currentX = ev.getRawX();
sheetDragHelper.processTouchEvent(ev);
return sheetDragHelper.getCapturedView() != null || super.onTouchEvent(ev);
}
这里获取的这个currentX是为了在下拉出现那个弧度的顶点。在接下来的回调中会使用。
private final ViewDragHelper.Callback dragHelperCallbacks = new ViewDragHelper.Callback() {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return child == sheet && !isExpanded();//是否可以拖拽
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
//竖直方向的值
return Math.min(Math.max(top, sheetExpandedTop), sheetBottom);
}
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
return sheet.getLeft();
}
@Override
public int getViewVerticalDragRange(View child) {
//竖直方向的拖拽范围
return sheetBottom - sheetExpandedTop;
}
@Override
public void onViewPositionChanged(View child, int left, int top, int dx, int dy) {
// view的拖拽过程中
reverse = false;
//change的过程中通知同步改变
sheetOffsetHelper.resyncOffsets();
dispatchPositionChangedCallback();
canUp = Math.abs(top - dismissOffset) > MIN_DRAG_DISTANCE;
}
@Override
public void onViewReleased(View releasedChild, float velocityX, float velocityY) {
//松手后
boolean expand = canUp || Math.abs(velocityY) > MIN_FLING_VELOCITY;
reverse = false;
animateSettle(expand ? sheetExpandedTop: dismissOffset, velocityY);
}
};
可以看到,在onViewPositionChanged()的方法中会去调用resyncOffsets()的方法同步ViewOffsetHelper的对应值。
在onViewReleased()的方法中调用了animateSettle()的方法,两种情况,一种是展开,一种是关闭(首页的状态),所以这里有一个expand的变量来标识,如果展开,就展开到sheetExpandedTop的高度,关闭的话,那么就是到dismissOffset的高度。
animateSettle()方法最终执行以下方法逻辑:
private void animateSettle(int initialOffset, final int targetOffset, float initialVelocity) {
if (settling) return;
Log.e(TAG, "animateSettle:TopAndBottom :::" + sheetOffsetHelper.getTopAndBottomOffset());
if (sheetOffsetHelper.getTopAndBottomOffset() == targetOffset) {
if (targetOffset >= dismissOffset) {
dispatchDismissCallback();
}
return;
}
settling = true;
final boolean dismissing = targetOffset == dismissOffset;
final long duration = computeSettleDuration(initialVelocity, dismissing);
final ObjectAnimator settleAnim = ObjectAnimator.ofInt(sheetOffsetHelper,
ViewOffsetHelper.OFFSET_Y,
initialOffset,
targetOffset);
settleAnim.setDuration(duration);
settleAnim.setInterpolator(getSettleInterpolator(dismissing, initialVelocity));
settleAnim.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
dispatchPositionChangedCallback();
if (dismissing) {
dispatchDismissCallback();
}
settling = false;
}
});
if (callbacks != null && !callbacks.isEmpty()) {
settleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if (animation.getAnimatedFraction() > 0f) {
dispatchPositionChangedCallback();
}
}
});
}
settleAnim.start();
}
这里有一个settleAnim的属性动画,传入的是ViewOffsetHelper里面的OFFSET_Y,在OFFSET_Y的set()方法中,调用setTopAndBottomOffset()的方法去修改对应的top值,从而实现了松手后展开或者关闭的动画效果。
final ObjectAnimator settleAnim = ObjectAnimator.ofInt(sheetOffsetHelper,
ViewOffsetHelper.OFFSET_Y,
initialOffset,
targetOffset);
public static final Property OFFSET_Y =
AnimUtils.createIntProperty(
new AnimUtils.IntProp("topAndBottomOffset") {
@Override
public void set(ViewOffsetHelper viewOffsetHelper, int offset) {
viewOffsetHelper.setTopAndBottomOffset(offset);
}
@Override
public int get(ViewOffsetHelper viewOffsetHelper) {
return viewOffsetHelper.getTopAndBottomOffset();
}
});
接下文
专栏作者简介( 点击 → 加入专栏作者 )
joe:90后程序猿。。
打赏支持作者写出更多好文章,谢谢!
关注「安卓开发精选」
看更多精选安卓技术文章
↓↓↓