话不多说先来个效果图看一下
实现的主要功能就是上拉抽屉(解决了子view的滑动冲突)+ 边缘动画 + 中间小球和seekbar效果动画。黄色部分就是上拉抽屉整体,绿色部分是横向的recyclerview。有个朋友说有阻尼效果就完美了 ... 因为效果图没有阻尼效果,所以就没有去研究 - -!
先总结一下主要用到的技术
- ScrollView + NestedScrollingParent + NestedScrollingChild (主要做上拉抽屉解决内部和外部滑动冲突的)
- 自定义view,贝塞尔曲线、lineTo、drawCircle、drawPath等一些常用的
emmmm 好像就没了,其实主要就是自定义view画图而已啦,也没有很复杂。
顶部也可以放个图片,像酱紫
圆形中间也可以放图片和文字,上下滑动的时候内部图片和文字也会随之改变,其实原理都是一样的,一个会了你放啥都行,文章后面也会介绍。
效果就是酱紫
抽屉里我放的是LinearLayout,然后动态添加了多个可以横向滚动的RecyclerView,上滑下滑左滑右滑轻松无压力~~就是这么刺激
效果介绍完了,下面我们看一下如何实现的
一、 上滑抽屉+抽屉内部滚动 解决上下滚动冲突
首先你得先了解NestedScrollingParent & NestedScrollingChild
主要就是父视图和子视图关于滚动的监听和相互之间滚动信号的传递。整理一下滚动的需求:
上滑
滚动父视图 - > 监听到顶之后 -> 滚动子视图
下滑
先滚动子视图 -> 子视图到顶后 -> 滚动父视图整体布局
父布局里是需要有三个子布局的
// 父布局的滚动
//需要上滑隐藏的部分
//上滑到顶需要吸附的部分
//子布局 内层滑动部分
在当前demo里
- 上滑隐藏的部分 :顶部透明
- 上滑到顶吸附的部分 :中间的弧度和圆
- ScrollParentView
- onStartNestedScroll 是否接受嵌套滚动,只有它返回true,后面 的其他方法才会被调用
- onNestedPreScroll 在内层view处理滚动事件前先被调用,可以让外层view先消耗部分滚动
- onNestedScroll 在内层view将剩下的滚动消耗完之后调用,可以在这里处理最后剩下的滚动
- onNestedPreFling 在内层view的Fling事件处理之前被调用
- onNestedFling 在内层view的Fling事件处理完之后调用
private View topView ;
private View centerView;
private View contentView;
private NestedScrollingParentHelper mParentHelper;
private int imgHeight;
private int tvHeight;
public ScrollParentView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ScrollParentView(Context context) {
super(context);
init();
}
/**
* 初始化内部三个子视图
*/
@Override
protected void onFinishInflate() {
super.onFinishInflate();
topView = getChildAt(0);
centerView = getChildAt(1);
contentView = getChildAt(2);
topView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if(imgHeight<=0){
imgHeight = topView.getMeasuredHeight();
}
}
});
centerView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if(tvHeight<=0){
tvHeight = centerView.getMeasuredHeight();
}
}
});
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(getMeasuredWidth(), topView.getMeasuredHeight() + centerView.getMeasuredHeight() + contentView.getMeasuredHeight());
}
public int getTopViewHeight(){
return topView.getMeasuredHeight();
}
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
return true;
}
private void init() {
mParentHelper = new NestedScrollingParentHelper(this);
}
@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {
mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);
}
@Override
public void onStopNestedScroll(View target) {
mParentHelper.onStopNestedScroll(target);
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
}
/**
* 处理上滑和下滑 顶部需要滚动的距离
* @param target
* @param dx
* @param dy
* @param consumed
*/
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
boolean headerScrollUp = dy > 0 && getScrollY() < imgHeight;
boolean headerScrollDown = dy < 0 && getScrollY() > 0 && !target.canScrollVertically(-1);
if (headerScrollUp || headerScrollDown) {
scrollBy(0, dy);
consumed[1] = dy;
}
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
return false;
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
return false;
}
@Override
public int getNestedScrollAxes() {
return 0;
}
@Override
public void scrollTo(int x, int y) {
if(y<0){
y=0;
}
if(y>imgHeight){
y=imgHeight;
}
super.scrollTo(x, y);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if(event.getAction()==MotionEvent.ACTION_DOWN){
return true;
}
return super.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
return super.onInterceptTouchEvent(event);
}
- ScrollChildView
子布局的滚动就相对比较简单,主要是通过代理处理和父布局的一些滚动事件
private NestedScrollingChildHelper mScrollingChildHelper;
public ScrollChildView(Context context) {
super(context);
init(context);
}
public ScrollChildView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public ScrollChildView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
public void init(Context context) {
final ViewConfiguration configuration = ViewConfiguration.get(context);
}
@Override
public void setNestedScrollingEnabled(boolean enabled) {
getScrollingChildHelper().setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return getScrollingChildHelper().isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
boolean bl = getScrollingChildHelper().startNestedScroll(axes);
return bl;
}
@Override
public void stopNestedScroll() {
getScrollingChildHelper().stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {
return getScrollingChildHelper().hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
return getScrollingChildHelper().dispatchNestedScroll(dxConsumed, dyConsumed,
dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return getScrollingChildHelper().dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return getScrollingChildHelper().dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return getScrollingChildHelper().dispatchNestedPreFling(velocityX, velocityY);
}
private NestedScrollingChildHelper getScrollingChildHelper() {
if (mScrollingChildHelper == null) {
mScrollingChildHelper = new NestedScrollingChildHelper(this);
mScrollingChildHelper.setNestedScrollingEnabled(true);
}
return mScrollingChildHelper;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
setMeasuredDimension(getMeasuredWidth(),getMeasuredHeight()+((ScrollParentView)getParent()).getTopViewHeight());
}
到这里就可以实现如效果图一样的滚动效果了
二、 类似水波纹的动画
这样看就比较直观些
这个就是用贝塞尔曲线画的简单的一个效果
- 首先 -> 了解贝塞尔曲线
已经有过很多人写了贝塞尔曲线的详解文章,学一下,这里不做详细介绍。
我这里是用了两个三阶贝塞尔曲线,从中间分开,左边一个右边一个,然后吧这个视图上下分为一半,中间的点不变,两边的高度增加,两边是扇形画的圆角,然后lineto画成封闭图形,这样就出现了如上图所示的动画效果。
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
mPath.reset();
// start point
mPath.moveTo(mStartX, mViewHeightHalf);
// 贝塞尔曲线
mPath.rCubicTo(mViewWidthHalf / 4, 0, mViewWidthHalf / 4, Math.abs(mViewHeightHalf - mCenterRadius), mViewWidthHalf / 2, Math.abs(mViewHeightHalf - mCenterRadius));
mPath.rCubicTo(mViewWidthHalf / 4, 0, mViewWidthHalf / 4, -Math.abs(mViewHeightHalf - mCenterRadius), mViewWidthHalf / 2, -Math.abs(mViewHeightHalf - mCenterRadius));
// 两边的圆角扇形
mPath.addArc(0, mViewHeightHalf, 200, mViewHeightHalf + 200, 180, 90);
mPath.addArc(mViewWidthHalf * 2 - 200, mViewHeightHalf, mViewWidthHalf * 2, mViewHeightHalf + 200, 270, 90);
// 图形边框
mPath.lineTo(this.getMeasuredWidth() - 100, mViewHeightHalf);
mPath.lineTo(this.getMeasuredWidth(), mViewHeightHalf + 100);
mPath.lineTo(this.getMeasuredWidth(), this.getMeasuredHeight());
mPath.lineTo(0, this.getMeasuredHeight());
mPath.lineTo(0, mViewHeightHalf + 100);
mPath.lineTo(100, mViewHeightHalf);
mPath.lineTo(mStartX, mViewHeightHalf);
mPath.lineTo(mStartX * 2 + mStartX, mViewHeightHalf);
mPath.setFillType(Path.FillType.WINDING);
//Close path
mPath.close();
canvas.drawPath(mPath, mCenterLinePaint);
}
三、圆形和圆环
这部分大家应该就比较熟悉,自定义view经常会用到,用法就不多说了,记录一下中间图片随之缩放和透明改变的写法
- Bitmap.createScaledBitmap 将当前存在的一个位图按一定比例(尺寸)构建一个新位图
- paint.setAlpha(mAlpha); 设置画笔的透明度
然后再动画中不断改变圆和圆环的半径、图的尺寸、画笔透明度,就能达到效果
四、整体上滑效果
抽屉的弧度、圆、圆环和图片这些的改变主要是监听当前上滑的距离和需要上滑的距离做的百分比计算的然后相应的随之改变。
mScrollParentView.setOnScrollChangeListener(new View.OnScrollChangeListener() {
@Override
public void onScrollChange(View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) {
float v1 = scrollY / topHeight;
if (0 <= v1 && v1 <= 1.1) {
mWaveView.changeWave(v1);
mCircleView.changeCircle(v1);
}
}
});
是在父view的滚动监听里做的改变,topHeight就是抽屉需要滚动的距离。
结语
之前接触的动画都是单独的模块,直接开始结束的那种,像这次这样需要动态改变而且多个结合的还是第一次遇到(渣渣本渣没错了),所以也是在边学边写,可能有很多地方写的不是很恰当,也是希望大佬可以指出,共同学习共同进步。其实现在的效果是大改过一次的,最初贝塞尔曲线高度取的整个高度,然后改变中间的那个点向下凹,但是外面的圆又要正好一半在他的上方一半在下方,这样的位置其实是不好做适配的,所以就改成了现在的这样。通过这个动画的实现,自己不仅是在自定义view、动画还是一些思考方式上都有所进步,这是挺重要的。项目中还有另一个动画,就下篇再讲吧~
gitee项目地址
https://gitee.com/yoyo666/TopScrollView.git