android仿音悅臺(tái)頁(yè)面交互效果實(shí)例代碼
概述
新版的音悅臺(tái) APP 播放頁(yè)面交互非常有意思,可以把播放器往下拖動(dòng),然后在底部懸浮一個(gè)小框,還可以左右拖動(dòng),然后回彈的時(shí)候也會(huì)有相應(yīng)的效果,這種交互效果在頭條視頻和一些專(zhuān)注于視頻的app也是很常見(jiàn)的。
前幾天看網(wǎng)友有仿這個(gè) 效果,覺(jué)得不錯(cuò),現(xiàn)在分享出來(lái),代碼可以再優(yōu)化,這里的播放器使用的是B站的ijkplayer,先上兩張動(dòng)圖。
當(dāng)圖片到達(dá)底部后,左右拖動(dòng)
實(shí)現(xiàn)的思路
首先,要是拖動(dòng)視圖縮小的效果,我們肯定需要自定義一個(gè)View,而根據(jù)我們項(xiàng)目的場(chǎng)景我們這里需要兩個(gè)View,一個(gè)是拖動(dòng)的View,另一個(gè)是浮動(dòng)上下的View(可以縮小的View),為了實(shí)現(xiàn)拖動(dòng),我們知道必定會(huì)用到ViewDragHelper這個(gè)類(lèi),這個(gè)類(lèi)專(zhuān)門(mén)為了拖動(dòng)而設(shè)計(jì)的。
然后,對(duì)于拖動(dòng)到底部的View,我們需要實(shí)現(xiàn)左右拖動(dòng)的效果,這個(gè)其實(shí)也是比較容易實(shí)現(xiàn)的,我們通過(guò)ViewDragHelper的onViewPositionChanged方法來(lái)判斷當(dāng)前視圖的狀況,就可以做View進(jìn)行縮放和漸變了。
代碼分析
首先我們會(huì)自定義一個(gè)容器,容器的init方法會(huì)初始化兩個(gè)View:mFlexView (到底拖動(dòng)的View)和mFollowView (跟隨觸摸縮放的View)
private void init(Context context, AttributeSet attrs) { final float density = getResources().getDisplayMetrics().density; final float minVel = MIN_FLING_VELOCITY * density; ViewGroupCompat.setMotionEventSplittingEnabled(this, false); FlexCallback flexCallback = new FlexCallback(); mDragHelper = ViewDragHelper.create(this, 1.0f, flexCallback); // 最小拖動(dòng)速度 mDragHelper.setMinVelocity(minVel); post(new Runnable() { @Override public void run() { // 需要添加的兩個(gè)子View,其中mFlexView作為拖動(dòng)的響應(yīng)View,mLinkView作為跟隨View mFlexView = getChildAt(0); mFollowView = getChildAt(1); mDragHeight = getMeasuredHeight() - mFlexView.getMeasuredHeight(); mFlexWidth = mFlexView.getMeasuredWidth(); mFlexHeight = mFlexView.getMeasuredHeight(); } }); }
ViewDragHelper 的回調(diào)需要做的事情比較多,在 mFlexView 拖動(dòng)的時(shí)候需要同時(shí)設(shè)置 mFlexView 和 mFollowView 的相應(yīng)變化效果,在 mFlexView 釋放的時(shí)候需要處理關(guān)閉或收起等效果。所以這里我們需要對(duì)ViewDragHelper個(gè)各種回調(diào)事件進(jìn)行監(jiān)聽(tīng)。這也是本功能最核心的:
private class FlexCallback extends ViewDragHelper.Callback { @Override public boolean tryCaptureView(View child, int pointerId) { // mFlexView來(lái)響應(yīng)觸摸事件 return mFlexView == child; } @Override public int clampViewPositionHorizontal(View child, int left, int dx) { return Math.max(Math.min(mDragWidth, left), -mDragWidth); } @Override public int getViewHorizontalDragRange(View child) { return mDragWidth * 2; } @Override public int clampViewPositionVertical(View child, int top, int dy) { if (!mVerticalDragEnable) { // 不允許垂直拖動(dòng)的時(shí)候是mFlexView在底部水平拖動(dòng)一定距離時(shí)設(shè)置的,返回mDragHeight就不能再垂直做拖動(dòng)了 return mDragHeight; } return Math.max(Math.min(mDragHeight, top), 0); } @Override public int getViewVerticalDragRange(View child) { return mDragHeight; } @Override public void onViewReleased(View releasedChild, float xvel, float yvel) { if (mHorizontalDragEnable) { // 如果水平拖動(dòng)有效,首先根據(jù)拖動(dòng)的速度決定關(guān)閉頁(yè)面,方向根據(jù)速度正負(fù)決定 if (xvel > 1500) { mDragHelper.settleCapturedViewAt(mDragWidth, mDragHeight); mIsClosing = true; } else if (xvel < -1500) { mDragHelper.settleCapturedViewAt(-mDragWidth, mDragHeight); mIsClosing = true; } else { // 速度沒(méi)到關(guān)閉頁(yè)面的要求,根據(jù)透明度來(lái)決定關(guān)閉頁(yè)面,方向根據(jù)releasedChild.getLeft()正負(fù)決定 float alpha = releasedChild.getAlpha(); if (releasedChild.getLeft() < 0 && alpha <= 0.4f) { mDragHelper.settleCapturedViewAt(-mDragWidth, mDragHeight); mIsClosing = true; } else if (releasedChild.getLeft() > 0 && alpha <= 0.4f) { mDragHelper.settleCapturedViewAt(mDragWidth, mDragHeight); mIsClosing = true; } else { mDragHelper.settleCapturedViewAt(0, mDragHeight); } } } else { // 根據(jù)垂直方向的速度正負(fù)決定布局的展示方式 if (yvel > 1500) { mDragHelper.settleCapturedViewAt(0, mDragHeight); } else if (yvel < -1500) { mDragHelper.settleCapturedViewAt(0, 0); } else { // 根據(jù)releasedChild.getTop()決定布局的展示方式 if (releasedChild.getTop() <= mDragHeight / 2) { mDragHelper.settleCapturedViewAt(0, 0); } else { mDragHelper.settleCapturedViewAt(0, mDragHeight); } } } invalidate(); } @Override public void onViewPositionChanged(final View changedView, int left, int top, int dx, int dy) { float fraction = top * 1.0f / mDragHeight; // mFlexView縮放的比率 mFlexScaleRatio = 1 - 0.5f * fraction; mFlexScaleOffset = changedView.getWidth() / 20; // 設(shè)置縮放基點(diǎn) changedView.setPivotX(changedView.getWidth() - mFlexScaleOffset); changedView.setPivotY(changedView.getHeight() - mFlexScaleOffset); // 設(shè)置比例 changedView.setScaleX(mFlexScaleRatio); changedView.setScaleY(mFlexScaleRatio); // mFollowView透明度的比率 float alphaRatio = 1 - fraction; // 設(shè)置透明度 mFollowView.setAlpha(alphaRatio); // 根據(jù)垂直方向的dy設(shè)置top,產(chǎn)生跟隨mFlexView的效果 mFollowView.setTop(mFollowView.getTop() + dy); // 到底部的時(shí)候,changedView的top剛好等于mDragHeight,以此作為水平拖動(dòng)的基準(zhǔn) mHorizontalDragEnable = top == mDragHeight; if (mHorizontalDragEnable) { // 如果水平拖動(dòng)允許的話(huà),由于設(shè)置縮放不會(huì)影響mFlexView的寬高(比如getWidth),所以水平拖動(dòng)距離為mFlexView寬度一半 mDragWidth = (int) (changedView.getMeasuredWidth() * 0.5f); // 設(shè)置mFlexView的透明度,這里向左右水平拖動(dòng)透明度都隨之變化 changedView.setAlpha(1 - Math.abs(left) * 1.0f / mDragWidth); // 水平拖動(dòng)一定距離的話(huà),垂直拖動(dòng)將被禁止 mVerticalDragEnable = left < 0 && left >= -mDragWidth * 0.05; } else { // 不是水平拖動(dòng)的處理 changedView.setAlpha(1); mDragWidth = 0; mVerticalDragEnable = true; } if (mFlexLayoutPosition == null) { // 創(chuàng)建子元素位置緩存 mFlexLayoutPosition = new ChildLayoutPosition(); mFollowLayoutPosition = new ChildLayoutPosition(); } // 記錄子元素的位置 mFlexLayoutPosition.setPosition(mFlexView.getLeft(), mFlexView.getRight(), mFlexView.getTop(), mFlexView.getBottom()); mFollowLayoutPosition.setPosition(mFollowView.getLeft(), mFollowView.getRight(), mFollowView.getTop(), mFollowView.getBottom()); // Log.e("FlexCallback", "225行-onViewPositionChanged(): 【" + mFlexView.getLeft() + ":" + mFlexView.getRight() + ":" + mFlexView.getTop() + ":" + mFlexView // .getBottom() + "】 【" + mFollowView.getLeft() + ":" + mFollowView.getRight() + ":" + mFollowView.getTop() + ":" + mFollowView.getBottom() + "】"); } }
接下來(lái)是處理測(cè)量和定位,我們實(shí)現(xiàn)的排列效果類(lèi)似 LinearLayout 垂直排列的效果,這里需要對(duì) measureChildWithMargins 的 heightUse 重新設(shè)置;onLayout 的時(shí)候在位置緩存不為空的時(shí)候直接定位是因?yàn)?ViewDragHelper 在處理觸摸事件子元素在做一些平移之類(lèi)的,若是有元素更新了 UI 會(huì)導(dǎo)致重新 Layout,因此在 FlexCallback 的 onViewPositionChanged 方法記錄位置,然后在回彈的時(shí)候需要通過(guò)Layout 恢復(fù)之前的視圖。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int desireHeight = 0; int desireWidth = 0; int tmpHeight = 0; if (getChildCount() != 2) { throw new IllegalArgumentException("只允許容器添加兩個(gè)子View!"); } if (getChildCount() > 0) { for (int i = 0; i < getChildCount(); i++) { final View child = getChildAt(i); // 測(cè)量子元素并考慮外邊距 // 參數(shù)heightUse:父容器豎直已經(jīng)被占用的空間,比如被父容器的其他子 view 所占用的空間;這里我們需要的是子View垂直排列,所以需要設(shè)置這個(gè)值 measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, tmpHeight); // 獲取子元素的布局參數(shù) final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); // 計(jì)算子元素寬度,取子控件最大寬度 desireWidth = Math.max(desireWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); // 計(jì)算子元素高度 tmpHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin; desireHeight += tmpHeight; } // 考慮父容器內(nèi)邊距 desireWidth += getPaddingLeft() + getPaddingRight(); desireHeight += getPaddingTop() + getPaddingBottom(); // 嘗試比較建議最小值和期望值的大小并取大值 desireWidth = Math.max(desireWidth, getSuggestedMinimumWidth()); desireHeight = Math.max(desireHeight, getSuggestedMinimumHeight()); } // 設(shè)置最終測(cè)量值 setMeasuredDimension(resolveSize(desireWidth, widthMeasureSpec), resolveSize(desireHeight, heightMeasureSpec)); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (mFlexLayoutPosition != null) { // 因?yàn)樵谟玫絍iewDragHelper處理布局交互的時(shí)候,若是有子View的UI更新導(dǎo)致重新Layout的話(huà),需要我們自己處理ViewDragHelper拖動(dòng)時(shí)子View的位置,否則會(huì)導(dǎo)致位置錯(cuò)誤 // Log.e("YytLayout1", "292行-onLayout(): " + "自己處理布局位置"); mFlexView.layout(mFlexLayoutPosition.getLeft(), mFlexLayoutPosition.getTop(), mFlexLayoutPosition.getRight(), mFlexLayoutPosition.getBottom()); mFollowView.layout(mFollowLayoutPosition.getLeft(), mFollowLayoutPosition.getTop(), mFollowLayoutPosition.getRight(), mFollowLayoutPosition.getBottom()); return; } final int paddingLeft = getPaddingLeft(); final int paddingTop = getPaddingTop(); int multiHeight = 0; int count = getChildCount(); if (count != 2) { throw new IllegalArgumentException("此容器的子元素個(gè)數(shù)必須為2!"); } for (int i = 0; i < count; i++) { // 遍歷子元素并對(duì)其進(jìn)行定位布局 final View child = getChildAt(i); MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); int left = paddingLeft + lp.leftMargin; int right = child.getMeasuredWidth() + left; int top = (i == 0 ? paddingTop : 0) + lp.topMargin + multiHeight; int bottom = child.getMeasuredHeight() + top; child.layout(left, top, right, bottom); multiHeight += (child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); } }
觸摸事件的處理,由于縮放不會(huì)影響 mFlexView 真實(shí)寬高,ViewDragHelper 仍然會(huì)阻斷 mFlexView 的真實(shí)寬高的區(qū)域,所以這里判斷手指是否落在 mFlexView 視覺(jué)上的范圍內(nèi),在才去調(diào) ViewDragHelper 的 shouldInterceptTouchEvent 方法。
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { // Log.e("YytLayout", mFlexView.getLeft() + ";" + mFlexView.getTop() + " --- " + ev.getX() + ":" + ev.getY()); // 由于縮放不會(huì)影響mFlexView真實(shí)寬高,這里手動(dòng)計(jì)算視覺(jué)上的范圍 float left = mFlexView.getLeft() + mFlexWidth * (1 - mFlexScaleRatio) - mFlexScaleOffset * (1 - mFlexScaleRatio); float top = mFlexView.getTop() + mFlexHeight * (1 - mFlexScaleRatio) - mFlexScaleOffset * (1 - mFlexScaleRatio); // 這里所做的是判斷手指是否落在mFlexView視覺(jué)上的范圍內(nèi) mInFlexViewTouchRange = ev.getX() >= left && ev.getY() >= top; if (mInFlexViewTouchRange) { return mDragHelper.shouldInterceptTouchEvent(ev); } else { return super.onInterceptTouchEvent(ev); } } @Override public boolean onTouchEvent(MotionEvent event) { if (mInFlexViewTouchRange) { // 這里還要做判斷是因?yàn)?,即使我不阻斷事件,但是此Layout的子View不消費(fèi)的話(huà),事件還是給回此Layout mDragHelper.processTouchEvent(event); return true; } else { // 不在mFlexView觸摸范圍內(nèi),并且子View沒(méi)有消費(fèi),返回false,把事件傳遞回去 return false; } }
同時(shí)我們需要對(duì)滾動(dòng)事件進(jìn)行監(jiān)聽(tīng),我們需要在此關(guān)閉的整個(gè)平移執(zhí)行事件。
@Override public void computeScroll() { if (mDragHelper.continueSettling(true)) { invalidate(); } else if (mIsClosing && mOnLayoutStateListener != null) { // 正在關(guān)閉的情況下,并且拖動(dòng)結(jié)束后,告知將要關(guān)閉頁(yè)面 mOnLayoutStateListener.onClose(); mIsClosing = false; } } /** * 監(jiān)聽(tīng)布局是否水平拖動(dòng)關(guān)閉了 */ public interface OnLayoutStateListener { void onClose(); } public void setOnLayoutStateListener(OnLayoutStateListener onLayoutStateListener) { mOnLayoutStateListener = onLayoutStateListener; } /** * 展開(kāi)布局 */ public void expand() { mDragHelper.smoothSlideViewTo(mFlexView, 0, 0); invalidate(); }
而在實(shí)際的應(yīng)用中要實(shí)現(xiàn)回彈后詳情頁(yè)面的效果,我們需要自己實(shí)現(xiàn)一個(gè)組合View,這個(gè)大家可以自己看源碼音悅臺(tái)源碼
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- android開(kāi)發(fā)之調(diào)用手機(jī)的攝像頭使用MediaRecorder錄像并播放
- Android實(shí)現(xiàn)歌曲播放時(shí)歌詞同步顯示具體思路
- Android提高之MediaPlayer播放網(wǎng)絡(luò)音頻的實(shí)現(xiàn)方法
- Android實(shí)現(xiàn)圖片循環(huán)播放的實(shí)例方法
- Android使用VideoView播放本地視頻和網(wǎng)絡(luò)視頻的方法
- 教你輕松制作Android音樂(lè)播放器
- Android提高之MediaPlayer音視頻播放
- android使用videoview播放視頻
- Android自定義播放器控件VideoView
- Android編程實(shí)現(xiàn)WebView全屏播放的方法(附源碼)
- Android編程開(kāi)發(fā)音樂(lè)播放器實(shí)例
相關(guān)文章
android獲取相冊(cè)圖片和路徑的實(shí)現(xiàn)方法
這篇文章主要介紹了android獲取相冊(cè)圖片和路徑的實(shí)現(xiàn)方法,本文介紹的是Android4.4后的方法,感興趣的小伙伴們可以參考一下2016-04-04Android自定義控件LinearLayout實(shí)例講解
這篇文章主要為大家詳細(xì)介紹了Android自定義控件LinearLayout實(shí)例,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-05-05Android高效加載大圖、多圖解決方案 有效避免程序OOM
這篇文章主要為大家詳細(xì)介紹了Android高效加載大圖、多圖解決方案,有效避免程序OOM,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-10-10淺談Android View繪制三大流程探索及常見(jiàn)問(wèn)題
下面小編就為大家?guī)?lái)一篇淺談Android View繪制三大流程探索及常見(jiàn)問(wèn)題。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-03-03Android TextView和ImageView簡(jiǎn)單說(shuō)明
Android TextView和ImageView簡(jiǎn)單說(shuō)明,需要的朋友可以參考一下2013-03-03Android中實(shí)現(xiàn)在矩形框中輸入文字顯示剩余字?jǐn)?shù)的功能
在矩形輸入框框中輸入文字顯示剩余字?jǐn)?shù)的功能在app開(kāi)發(fā)中經(jīng)常會(huì)見(jiàn)到,今天小編就通過(guò)實(shí)例代碼給大家分享android實(shí)現(xiàn)輸入框提示剩余字?jǐn)?shù)功能,代碼簡(jiǎn)單易懂,需要的朋友參考下吧2017-04-04Android SDK Manager解決更新時(shí)的問(wèn)題 :Failed to fetch URL...
本文主要介紹解決安裝使用SDK Manager更新時(shí)的問(wèn)題:Failed to fetch URL...,這里提供了詳細(xì)的資料及解決問(wèn)題辦法,有需要的小伙伴可以參考下2016-09-09Android Studio里如何使用lambda表達(dá)式
這篇文章主要介紹了Android Studio里如何使用lambda表達(dá)式,需要的朋友可以參考下2017-05-05AndroidStudio升級(jí)4.1坑(無(wú)法啟動(dòng)、插件plugin不好用、代碼不高亮)
這篇文章主要介紹了AndroidStudio升級(jí)4.1坑(無(wú)法啟動(dòng)、插件plugin不好用、代碼不高亮),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10