話不多說先來個(gè)效果圖看一下
實(shí)現(xiàn)的主要功能就是上拉抽屜(解決了子view的滑動(dòng)沖突)+ 邊緣動(dòng)畫 + 中間小球和seekbar效果動(dòng)畫兜材。黃色部分就是上拉抽屜整體,綠色部分是橫向的recyclerview据沈。有個(gè)朋友說有阻尼效果就完美了 ... 因?yàn)樾Ч麍D沒有阻尼效果,所以就沒有去研究 - -!
先總結(jié)一下主要用到的技術(shù)
- ScrollView + NestedScrollingParent + NestedScrollingChild (主要做上拉抽屜解決內(nèi)部和外部滑動(dòng)沖突的)
- 自定義view戒悠,貝塞爾曲線、lineTo钞螟、drawCircle兔甘、drawPath等一些常用的
emmmm 好像就沒了,其實(shí)主要就是自定義view畫圖而已啦鳞滨,也沒有很復(fù)雜洞焙。
頂部也可以放個(gè)圖片,像醬紫
圓形中間也可以放圖片和文字拯啦,上下滑動(dòng)的時(shí)候內(nèi)部圖片和文字也會(huì)隨之改變澡匪,其實(shí)原理都是一樣的,一個(gè)會(huì)了你放啥都行提岔,文章后面也會(huì)介紹仙蛉。
效果就是醬紫
抽屜里我放的是LinearLayout,然后動(dòng)態(tài)添加了多個(gè)可以橫向滾動(dòng)的RecyclerView碱蒙,上滑下滑左滑右滑輕松無壓力~~就是這么刺激
效果介紹完了荠瘪,下面我們看一下如何實(shí)現(xiàn)的
一、 上滑抽屜+抽屜內(nèi)部滾動(dòng) 解決上下滾動(dòng)沖突
首先你得先了解NestedScrollingParent & NestedScrollingChild
主要就是父視圖和子視圖關(guān)于滾動(dòng)的監(jiān)聽和相互之間滾動(dòng)信號(hào)的傳遞赛惩。整理一下滾動(dòng)的需求:
上滑
滾動(dòng)父視圖 - > 監(jiān)聽到頂之后 -> 滾動(dòng)子視圖
下滑
先滾動(dòng)子視圖 -> 子視圖到頂后 -> 滾動(dòng)父視圖整體布局
父布局里是需要有三個(gè)子布局的
// 父布局的滾動(dòng)
<com.yoyo.topscrollview.scrollview.ScrollParentView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:orientation="vertical"
android:id="@+id/scrollParentView">
//需要上滑隱藏的部分
<RelativeLayout
android:id="@+id/rl_transparentTop"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
//上滑到頂需要吸附的部分
<RelativeLayout
android:id="@+id/center"
android:layout_width="match_parent"
android:layout_height="100dp">
<com.yoyo.topscrollview.centerview.WaveView
android:id="@+id/waveView"
android:layout_centerInParent="true"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<com.yoyo.topscrollview.centerview.CircleView
android:id="@+id/circleView"
android:layout_centerInParent="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:ring_color="@color/lightPink"
app:circle_color="@color/pink"/>
</RelativeLayout>
//子布局 內(nèi)層滑動(dòng)部分
<com.yoyo.topscrollview.scrollview.ScrollChildView
android:id="@+id/scrollChildView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:scrollbars="none"
android:overScrollMode="never">
<LinearLayout
android:id="@+id/ll_content"
android:background="@color/orange"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingLeft="15dp"
android:paddingRight="15dp">
</LinearLayout>
</com.yoyo.topscrollview.scrollview.ScrollChildView>
</com.yoyo.topscrollview.scrollview.ScrollParentView>
在當(dāng)前demo里
- 上滑隱藏的部分 :頂部透明
- 上滑到頂吸附的部分 :中間的弧度和圓
- ScrollParentView
- onStartNestedScroll 是否接受嵌套滾動(dòng),只有它返回true,后面 的其他方法才會(huì)被調(diào)用
- onNestedPreScroll 在內(nèi)層view處理滾動(dòng)事件前先被調(diào)用,可以讓外層view先消耗部分滾動(dòng)
- onNestedScroll 在內(nèi)層view將剩下的滾動(dòng)消耗完之后調(diào)用,可以在這里處理最后剩下的滾動(dòng)
- onNestedPreFling 在內(nèi)層view的Fling事件處理之前被調(diào)用
- onNestedFling 在內(nèi)層view的Fling事件處理完之后調(diào)用
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();
}
/**
* 初始化內(nèi)部三個(gè)子視圖
*/
@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) {
}
/**
* 處理上滑和下滑 頂部需要滾動(dòng)的距離
* @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
子布局的滾動(dòng)就相對(duì)比較簡單哀墓,主要是通過代理處理和父布局的一些滾動(dòng)事件
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());
}
到這里就可以實(shí)現(xiàn)如效果圖一樣的滾動(dòng)效果了
二、 類似水波紋的動(dòng)畫
這樣看就比較直觀些
這個(gè)就是用貝塞爾曲線畫的簡單的一個(gè)效果
- 首先 -> 了解貝塞爾曲線
已經(jīng)有過很多人寫了貝塞爾曲線的詳解文章喷兼,學(xué)一下篮绰,這里不做詳細(xì)介紹。
我這里是用了兩個(gè)三階貝塞爾曲線季惯,從中間分開吠各,左邊一個(gè)右邊一個(gè),然后吧這個(gè)視圖上下分為一半勉抓,中間的點(diǎn)不變贾漏,兩邊的高度增加,兩邊是扇形畫的圓角藕筋,然后lineto畫成封閉圖形纵散,這樣就出現(xiàn)了如上圖所示的動(dòng)畫效果。
@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);
}
三隐圾、圓形和圓環(huán)
這部分大家應(yīng)該就比較熟悉伍掀,自定義view經(jīng)常會(huì)用到,用法就不多說了暇藏,記錄一下中間圖片隨之縮放和透明改變的寫法
- Bitmap.createScaledBitmap 將當(dāng)前存在的一個(gè)位圖按一定比例(尺寸)構(gòu)建一個(gè)新位圖
- paint.setAlpha(mAlpha); 設(shè)置畫筆的透明度
然后再動(dòng)畫中不斷改變圓和圓環(huán)的半徑蜜笤、圖的尺寸、畫筆透明度盐碱,就能達(dá)到效果
四把兔、整體上滑效果
抽屜的弧度啊胶、圓、圓環(huán)和圖片這些的改變主要是監(jiān)聽當(dāng)前上滑的距離和需要上滑的距離做的百分比計(jì)算的然后相應(yīng)的隨之改變垛贤。
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的滾動(dòng)監(jiān)聽里做的改變,topHeight就是抽屜需要滾動(dòng)的距離趣倾。
結(jié)語
之前接觸的動(dòng)畫都是單獨(dú)的模塊聘惦,直接開始結(jié)束的那種,像這次這樣需要?jiǎng)討B(tài)改變而且多個(gè)結(jié)合的還是第一次遇到(渣渣本渣沒錯(cuò)了)儒恋,所以也是在邊學(xué)邊寫善绎,可能有很多地方寫的不是很恰當(dāng),也是希望大佬可以指出诫尽,共同學(xué)習(xí)共同進(jìn)步禀酱。其實(shí)現(xiàn)在的效果是大改過一次的,最初貝塞爾曲線高度取的整個(gè)高度牧嫉,然后改變中間的那個(gè)點(diǎn)向下凹剂跟,但是外面的圓又要正好一半在他的上方一半在下方,這樣的位置其實(shí)是不好做適配的酣藻,所以就改成了現(xiàn)在的這樣曹洽。通過這個(gè)動(dòng)畫的實(shí)現(xiàn),自己不僅是在自定義view辽剧、動(dòng)畫還是一些思考方式上都有所進(jìn)步送淆,這是挺重要的。項(xiàng)目中還有另一個(gè)動(dòng)畫怕轿,就下篇再講吧~
gitee項(xiàng)目地址
https://gitee.com/yoyo666/TopScrollView.git