最近开发遇到一个需求,要仿微信下拉出现类似小程序的页面,拿到需求第一时间当然是网上先搜索一通,最后发现了一个博主写过一个高仿版的,参考高仿微信下拉小程序入口动画 - 掘金。但是,由于这篇文章里面的代码是kotlin的,本人对kotlin不是很精通,我们自己的项目又是java语言的,所以我就在此基础上,照葫芦画瓢,搞了一个java版的。
主要的两个类就是下面这两个:
public class WXMainPullViewGroup extends ViewGroup {
public ViewDragHelper viewDragHelper = ViewDragHelper.create(this, 0.5f, new DragHandler());
WXPullHeaderMaskView headerMaskView;
boolean isOpen = false;
int NAVIGATION_HEIGHT = 100;
private RecyclerView recyclerView;
private int bottomMargin = DisplayUtil.dp2px(AppApplication.getContext(),12);
public WXMainPullViewGroup(Context context) {
super(context);
}
public WXMainPullViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
public WXMainPullViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
ViewDragHelper viewDragHelper = ViewDragHelper.create((ViewGroup) this, 0.9F, (ViewDragHelper.Callback) (new WXMainPullViewGroup.DragHandler()));
this.viewDragHelper = viewDragHelper;
NAVIGATION_HEIGHT = 100;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
if (getChildAt(i) != headerMaskView) {
getChildAt(i).layout(l, getPaddingTop(), r, b);
}
}
//Log.i("hty", "onLayout");
}
@Override
public void computeScroll() {
super.computeScroll();
if (viewDragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
}
//Log.i("hty", "computeScroll");
}
//这里接收的是子布局中的可滑动的布局,可能是NestedScrollView、RecycleView,ListView等
public void setChildScrollView(RecyclerView recyclerView) {
this.recyclerView = recyclerView;
//Log.i("hty", "setChildScrollView");
}
float mDownY;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// if (isOpen) {
// return true;
// }
//Log.i("hty", "onInterceptTouchEvent:"+ev.getAction());
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
mDownY = ev.getY();
viewDragHelper.processTouchEvent(ev);
// Log.e("hty", "mDownY: " + mDownY);
// Log.e("hty", "isOpen: " + isOpen);
break;
case MotionEvent.ACTION_MOVE:
float moveY = ev.getY();
// Log.e("hty", "moveY: " + moveY);
// Log.e("hty", " scroll is " + nestedScrollView.canScrollVertically(-1));
if ((moveY - mDownY) > 4 && !recyclerView.canScrollVertically(-1)) {
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
public boolean onTouchEvent(MotionEvent event) {
//Log.i("hty", "onTouchEvent y : " + event.getY());
this.viewDragHelper.processTouchEvent(event);
return true;
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Long startTime = System.currentTimeMillis();
this.measureChildren(widthMeasureSpec, heightMeasureSpec);
//Log.i("hty", "onMeasure 耗时: " + (System.currentTimeMillis()-startTime));
}
public final void createMaskView() {
if (this.headerMaskView == null) {
this.headerMaskView = new WXPullHeaderMaskView(this.getContext(), (AttributeSet) null, 0);
this.addView((View) this.headerMaskView);
}
}
public void setOpen(boolean open) {
isOpen = open;
}
public boolean getOpen(){
return isOpen;
}
class DragHandler extends ViewDragHelper.Callback {
@Override
public boolean tryCaptureView(@NonNull View child, int pointerId) {
return child instanceof FrameLayout;
}
@Override
public void onViewDragStateChanged(int state) {
super.onViewDragStateChanged(state);
//Log.e("hty", "onViewDragStateChanged....state "+state);
}
@Override
public void onViewPositionChanged(@NonNull View changedView, int left, int top, int dx, int dy) {
super.onViewPositionChanged(changedView, left, top, dx, dy);
//Log.i("hty", "onViewPositionChanged start...");
createMaskView();
View programView = getChildAt(0);
BigDecimal divide = (new BigDecimal(String.valueOf(top))).divide(new BigDecimal(getMeasuredHeight() - NAVIGATION_HEIGHT), 4, 4);
divide = divide.multiply(new BigDecimal("100"));
divide = divide.multiply(new BigDecimal("0.002"));
divide = divide.add(new BigDecimal("0.8"));
if (!isOpen) {
programView.setScaleX(divide.floatValue());
programView.setScaleY(divide.floatValue());
} else {
programView.setTop(getPaddingTop() + (-((getMeasuredHeight() - NAVIGATION_HEIGHT) - top)));
}
WXPullHeaderMaskView headerMaskView1 = headerMaskView;
if (headerMaskView1 == null) {
return;
}
headerMaskView1.maxHeight = getMeasuredHeight() / 3;
headerMaskView1.layout(0, getPaddingTop(), getMeasuredWidth(), top);
float progress = Float.parseFloat(top + "") / Float.parseFloat(((getMeasuredHeight() - (NAVIGATION_HEIGHT + getPaddingTop())) / 3) + "") * 100;
headerMaskView1.setProgress(progress, getMeasuredHeight() - (NAVIGATION_HEIGHT + getPaddingTop()));
if (onProgressListener != null) {
onProgressListener.onProgress(progress);
}
if (top == getPaddingTop()) {
isOpen = false;
// Log.e("hty","onViewPositionChanged isOpen = false");
}
//Log.i("hty", "onViewPositionChanged getMeasuredHeight: " + getMeasuredHeight());
if (top == getMeasuredHeight() - NAVIGATION_HEIGHT - bottomMargin) {
isOpen = true;
// Log.e("hty","onViewPositionChanged isOpen = true");
}
// Log.i("hty", "onViewPositionChanged end... isopen is " + isOpen);
}
public void onViewCaptured(View capturedChild, int activePointerId) {
super.onViewCaptured(capturedChild, activePointerId);
View programView = getChildAt(0);
programView.setTop(getPaddingTop());
}
public void onViewReleased(View releasedChild, float xvel, float yvel) {
// Log.e("hty", "onViewReleased start... isopen is " + isOpen);
releaseChild = releasedChild;
if (isOpen || releasedChild.getTop() + getPaddingTop() <= getMeasuredHeight() / 3) {
viewDragHelper.smoothSlideViewTo(releasedChild, 0, getPaddingTop());
ViewCompat.postInvalidateOnAnimation((View) WXMainPullViewGroup.this);
//收起 隐藏小程序页面,显示底部菜单
if (onPageChangListener != null) {
onPageChangListener.onPageClosed();
}
} else {
//此处需要加上首页底部菜单的高度
int bottom = Math.round(AppApplication.getContext().getResources().getDimension(R.dimen.iconsize_58));
// Log.e("hty", "onViewReleased2 bottom:" + bottom);
// Log.e("hty", "getMeasuredHeight:" + getMeasuredHeight());
viewDragHelper.smoothSlideViewTo(releasedChild, 0, getMeasuredHeight() + bottom - NAVIGATION_HEIGHT - bottomMargin);
ViewCompat.postInvalidateOnAnimation((View) WXMainPullViewGroup.this);
//展开 显示小程序页面,并且隐藏菜单底部
if (onPageChangListener != null) {
onPageChangListener.onPageOpened();
}
}
}
public int clampViewPositionVertical(View child, int top, int dy) {
return top <= getPaddingTop() ? getPaddingTop() : (int) ((double) child.getTop() + (double) dy / 1.3D);
}
}
private View releaseChild;
public void resumeView() {
if (releaseChild != null && viewDragHelper != null) {
viewDragHelper.smoothSlideViewTo(releaseChild, 0, getPaddingTop());
ViewCompat.postInvalidateOnAnimation((View) WXMainPullViewGroup.this);
}
}
private OnPageChangListener onPageChangListener;
public interface OnPageChangListener {
void onPageOpened();
void onPageClosed();
}
public void setOnPageChangeListener(OnPageChangListener onPageChangeListener) {
this.onPageChangListener = onPageChangeListener;
}
private OnProgressListener onProgressListener;
public interface OnProgressListener {
void onProgress(float progress);
}
public void setOnProgressListener(OnProgressListener onProgressListener) {
this.onProgressListener = onProgressListener;
}
}
public class WXPullHeaderMaskView extends View {
private boolean isVibrator = false;
private int progress = 0;
public int maxHeight = 0;
private float CIRCLE_MAX_SIZE = 32;
private int parentHeight = 0;
private Paint paint = new Paint();
private float DEFAULT_CIRCLE_SIZE = 8f;
private Context context;
public WXPullHeaderMaskView(Context context) {
super(context);
}
public WXPullHeaderMaskView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public WXPullHeaderMaskView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
private void init(Context context) {
this.context = context;
setBackgroundColor(Color.argb(255, 40, 40, 40));
paint.setAlpha(255);
paint.setAntiAlias(true);
paint.setColor(ContextCompat.getColor(context, R.color.color_grey_d0d0d0));
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float width = Float.parseFloat(getWidth() + "");
float height = Float.parseFloat(getHeight() + "");
float value = height / maxHeight;
if (height <= maxHeight / 2) {
canvas.drawCircle(width / 2, height / 2, CIRCLE_MAX_SIZE * value, paint);
} else {
if (progress < 100) {
float diff = (value - 0.5f) * CIRCLE_MAX_SIZE;
canvas.drawCircle((width / 2 - ((0.4f - value) * 100)), height / 2, DEFAULT_CIRCLE_SIZE, paint);
canvas.drawCircle((width / 2 + ((0.4f - value) * 100)), height / 2, DEFAULT_CIRCLE_SIZE, paint);
if ((CIRCLE_MAX_SIZE * 0.5f) - diff <= DEFAULT_CIRCLE_SIZE) {
canvas.drawCircle(width / 2, height / 2, DEFAULT_CIRCLE_SIZE, paint);
} else {
canvas.drawCircle(width / 2, height / 2, (CIRCLE_MAX_SIZE * 0.5f) - diff, paint);
}
} else {
paint.setAlpha(getAlphaValue());
canvas.drawCircle(width / 2, height / 2, DEFAULT_CIRCLE_SIZE, paint);
canvas.drawCircle(width / 2 - ((0.4f) * 100), height / 2, DEFAULT_CIRCLE_SIZE, paint);
canvas.drawCircle(width / 2 + (((0.4f) * 100)), height / 2, DEFAULT_CIRCLE_SIZE, paint);
}
}
}
private int getAlphaValue() {
int dc = parentHeight / 3 - CheckBoxUtil.getStatusBarHeight();
float alpha = (Float.parseFloat(getHeight() + "") - dc) / (parentHeight - (dc));
// Log.e("hty", "dc:" + dc + " alpha:" + alpha);
return 255 - Math.round(255 * alpha);
}
private void vibrator() {
Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
VibrationEffect createOneShot = VibrationEffect.createOneShot(7, 255);
vibrator.vibrate(createOneShot);
} else {
vibrator.vibrate(7);
}
}
public void setProgress(float value, int parentHeight) {
// Log.e("hty", "progress: " + value);
this.progress = Math.round(value);
this.parentHeight = parentHeight;
if (value >= 100 && !isVibrator) {
vibrator();
isVibrator = true;
}
if (value < 100) {
isVibrator = false;
}
if (progress >= 100) {
// Log.e("hty", "alpha:" + getAlphaValue());
// setBackgroundColor(Color.argb(getAlphaValue(), 40, 40, 40));
setAlpha(Float.parseFloat(getAlphaValue()+"")/255);
} else {
// setBackgroundColor(Color.argb(255, 40, 40, 40));
setAlpha(1);
boolean isDark = DarkModeUtils.spDark(context);
if (!isDark) {
setBackgroundResource(R.drawable.status_bar_bg_selector);
} else {
setBackgroundResource(R.drawable.status_bar_night_bg_selector);
}
}
invalidate();
}
}
使用方式也很简单,在布局文件中直接引用就行。
注意:给布局直接添加两个child布局,第一个是下拉要展示的布局,第二个是当前看到的布局,布局类型自己定义。由于第二个布局我需要用FrameLayout,所以我就在WXMainPullViewGroup.java中的 tryCaptureView方法中返回此布局。
由于我的第二个布局里面用到了Recycleview,我就处理了Recycleview和ViewDragHelper的滑动冲突问题,可以借鉴。同时,我还添加了页面滑动进度的回调和打开关闭的回调,可以直接监听。切记,Recycleview外层不要嵌套scrollview了,在布局重绘计算的时候,会非常的卡顿。