Android中怎么實(shí)現(xiàn)下拉阻尼效果,很多新手對(duì)此不是很清楚,為了幫助大家解決這個(gè)難題,下面小編將為大家詳細(xì)講解,有這方面需求的人可以來學(xué)習(xí)下,希望你能有所收獲。
創(chuàng)新互聯(lián)專業(yè)提供鄭州服務(wù)器托管服務(wù),為用戶提供五星數(shù)據(jù)中心、電信、雙線接入解決方案,用戶可自行在線購(gòu)買鄭州服務(wù)器托管服務(wù),并享受7*24小時(shí)金牌售后服務(wù)。
原理
這種效果是通過自定義控件的方式來實(shí)現(xiàn)的,我自定義了一個(gè)控件類型,這個(gè)自定義控件(PullDownDumperLayout)繼承自線性布局(LinearLayout)。
用戶可以下拉彈出的那個(gè)視圖,例如微信的小程序列表,開發(fā)者只是將這個(gè)視圖移出了父元素之外,所以不可見,我們暫且稱之為隱藏頭部,只有下拉到一定程度才會(huì)彈出,而主體,例如微信的聯(lián)系人列表,則是可見的,布局見下圖。
實(shí)現(xiàn)這個(gè)效果需要我們做三件工作:
1.隱藏作為頭部的控件2.監(jiān)聽用戶對(duì)屏幕的操作事件3.實(shí)現(xiàn)下拉回彈的動(dòng)畫效果
我們這個(gè)自定義控件會(huì)自動(dòng)獲取內(nèi)部第一個(gè)子元素充當(dāng)頭部,其余的元素則是充當(dāng)可見的主體(詳見代碼中的注釋)。
基本的布局原理差不多就這樣了,但是我們還需要讓自定義控件監(jiān)聽用戶的手勢(shì)操作,例如上下滑動(dòng)等。這里我和靈感來源的那篇博客一樣,讓自定義控件實(shí)現(xiàn)View.OnTouchListener接口,實(shí)現(xiàn)內(nèi)部的onTouch方法可以監(jiān)聽來自屏幕的所有觸摸操作。代碼中我讓頭部和第二個(gè)子元素(可見的主體)注冊(cè)了這個(gè)監(jiān)聽器,這是為了方便讀者理解,讀者可根據(jù)自己的需求進(jìn)行修改。
注意,對(duì)于不能監(jiān)聽屏幕觸摸事件的控件需要添加:
android:clickable="true"
至此,我們已經(jīng)可以進(jìn)行布局和監(jiān)聽用戶手勢(shì)了,但是還需要實(shí)現(xiàn)一個(gè)頭部展開和隱藏的動(dòng)畫效果。當(dāng)用戶將隱藏頭部下拉或上滑到一定高度時(shí),這個(gè)效果就會(huì)被觸發(fā),這需要依賴上面所述的onTouch方法。動(dòng)畫效果的實(shí)現(xiàn)需要另開一個(gè)線程進(jìn)行操作,線程的啟動(dòng)方式我們可以采用繼承AsyncTask類來實(shí)現(xiàn)。
除此之外,我們可能會(huì)多次復(fù)用這個(gè)控件,所以在自定義控件類的最后還需要一些調(diào)整參數(shù)的set方法。
這里提個(gè)醒,在接下來的代碼中,我們的自定義控件因?yàn)槔^承自LinearLayout,里面需要重寫onLayout方法,而onLayout方法顧名思義就是布局,這個(gè)方法在Activity中的onCreate方法執(zhí)行之后才會(huì)被調(diào)用,所以我們可以在Activity的onCreate方法中利用findViewById獲取實(shí)例,調(diào)用上面提到的set方法進(jìn)行參數(shù)的初始化。
LinearLayout中不止onLayout一個(gè)方法,詳細(xì)解析請(qǐng)讀者移步其他關(guān)于XML標(biāo)簽加載過程的文章,這里不做贅述。
代碼
PullDownDumperLayout .java:
public class PullDownDumperLayout extends LinearLayout implements View.OnTouchListener { /** * 取布局中的第一個(gè)子元素為下拉隱藏頭部 */ private View mHeadLayout; /** * 隱藏頭部布局的高的負(fù)值 */ private int mHeadLayoutHeight; /** * 隱藏頭部的布局參數(shù) */ private MarginLayoutParams mHeadLayoutParams; /** * 判斷是否為第一次初始化,第一次初始化需要把headView移出界面外 */ private boolean mOnLayoutIsInit=false; /** * 移動(dòng)時(shí),前一個(gè)坐標(biāo) */ private float mMoveY; /** * 如果為false,會(huì)退出頭部展開或隱藏動(dòng)畫 */ private boolean mChangeHeadLayoutTopMargin; /** * 觸發(fā)動(dòng)畫的分界線,由mRatio計(jì)算得到 */ private int mBoundary; /** * 頭部布局的隱藏和展開速度,以及單次執(zhí)行時(shí)間 */ private int mHeadLayoutHideSpeed; private int mHeadLayoutUnfoldSpeed; private long mSleepTime; /** * 觸發(fā)動(dòng)畫的分界線,頭部布局上半部分和整體高度的比例 */ private double mRatio; public PullDownDumperLayout(Context context, AttributeSet attrs) { super(context, attrs); //初始化參數(shù),根據(jù)自己的需求調(diào)整 mHeadLayoutHideSpeed=-20; mHeadLayoutUnfoldSpeed=20; mSleepTime=10; mRatio=0.5; } /** * 布局開始設(shè)置每一個(gè)控件 * 在activity的onCreate執(zhí)行之后才會(huì)執(zhí)行 * 因此可以在onCreate中調(diào)用set方法設(shè)置參數(shù) */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); if(!mOnLayoutIsInit && changed) { //將第一個(gè)子元素作為頭部移出界面外 mHeadLayout = this.getChildAt(0); mHeadLayoutHeight=-mHeadLayout.getHeight(); mBoundary=(int)(mRatio*mHeadLayoutHeight);//計(jì)算觸發(fā)動(dòng)畫分界線 mHeadLayoutParams=(MarginLayoutParams) mHeadLayout.getLayoutParams(); mHeadLayoutParams.topMargin=mHeadLayoutHeight; mHeadLayout.setLayoutParams(mHeadLayoutParams); //TODO 設(shè)置手勢(shì)監(jiān)聽器,不能觸碰的控件需要添加android:clickable="true" getChildAt(1).setOnTouchListener(this); mHeadLayout.setOnTouchListener(this); //標(biāo)記已被初始化 mOnLayoutIsInit=true; } } /** * 屏幕觸摸操作監(jiān)聽器 * @return false則注冊(cè)本監(jiān)聽器的控件將不會(huì)對(duì)事件做出響應(yīng),true則相反 */ @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: mMoveY=event.getRawY();//捕獲按下時(shí)的坐標(biāo),初始化mMoveY mChangeHeadLayoutTopMargin=false; break; case MotionEvent.ACTION_MOVE: float currY=event.getRawY(); int vector=(int)(currY-mMoveY);//向量,用于判斷手勢(shì)的上滑和下滑 mMoveY=currY; //判斷是否為滑動(dòng) if(Math.abs(vector)==0){ return false; } //頭部完全隱藏時(shí)不再向上滑動(dòng) if (vector < 0 && mHeadLayoutParams.topMargin <= mHeadLayoutHeight) { return false; } //頭部完全展開時(shí)不再向下滑動(dòng) if (vector > 0 && mHeadLayoutParams.topMargin >= 0) { return false; } //對(duì)增量進(jìn)行修正,對(duì)滑動(dòng)距離進(jìn)行減半 int topMargin = mHeadLayoutParams.topMargin + (vector/2);//阻尼值 if(topMargin>0){ // 瞬間拉動(dòng)的距離超過了頭部高度,因?yàn)檫@一瞬間很短,這里采用直接賦值的方式 // 如需平滑過渡,要另開線程,并且監(jiān)聽到ACTION_DOWN時(shí)線程可被打斷 topMargin = 0; } else if(topMargin
activity_main.xml:
MainActivity.java:
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //TODO 讀者可在這里初始化參數(shù) PullDownDumperLayout pddl=findViewById(R.id.PullDownDumper); }}
下面是筆者正在使用的自定義控件,比上述的控件多了一個(gè)效果:
頭部處于隱藏或展開的不同狀態(tài)時(shí),觸發(fā)動(dòng)畫效果的分界線可以隨狀態(tài)不同而改變。
還是拿最新版的微信小程序入口來講,用戶在下拉時(shí),小程序界面會(huì)占用整個(gè)屏幕,如果觸發(fā)動(dòng)畫的分界線太低,這樣導(dǎo)致的結(jié)果是用戶可能無法通過上滑重新返回聯(lián)系人列表,但由于微信沒有對(duì)滑動(dòng)距離進(jìn)行減半處理,所以不存在上述問題,可能是出于防止誤觸的原因,從小程序界面返回聯(lián)系人列表的方式改用點(diǎn)擊底部的一個(gè)按鈕。而我的控件可以通過改變觸發(fā)動(dòng)畫效果的分界線來解決這一問題,感興趣的讀者可以研究一下。
public class PullDownDumperLayout extends LinearLayout implements View.OnTouchListener { /** * 取布局中的第一個(gè)子元素為下拉隱藏頭部 */ private View mHeadLayout; /** * 隱藏頭部布局的高的負(fù)值 */ private int mHeadLayoutHeight; /** * 隱藏頭部的布局參數(shù) */ private MarginLayoutParams mHeadLayoutParams; /** * 判斷是否為第一次初始化,第一次初始化需要把headView移出界面外 */ private boolean mOnLayoutIsInit=false; /** * 從配置獲取的滾動(dòng)判斷閾值,為兩點(diǎn)間的距離,超過此閾值判斷為滾動(dòng) */// private int mScaledTouchSlop; /** * 按下時(shí)的y軸坐標(biāo) */// private float mDownY; /** * 移動(dòng)時(shí),前一個(gè)坐標(biāo) */ private float mMoveY; /** * 如果為false,會(huì)退出頭部展開或隱藏動(dòng)畫 */ private boolean mChangeHeadLayoutTopMargin; /** * 頭部布局的隱藏和展開速度,以及單次執(zhí)行時(shí)間 */ private int mHeadLayoutHideSpeed; private int mHeadLayoutUnfoldSpeed; private long mSleepTime; /** * 初始化頭部布局的偏移值,數(shù)值越大,頭部可見部分越多,預(yù)設(shè)值為0,即初始時(shí)頭部完全不可見 */ private int mTopMarginOffset; /** * 觸發(fā)動(dòng)畫的分界線,頭部布局上半部分和整體高度的比例 */ private double mUnfoldRatio; private double mHideRatio; /** * 觸發(fā)動(dòng)畫的分界線,初始值由mRatio計(jì)算得到 * 頭部處于隱藏時(shí)等于mUnfoldBoundary * 頭部處于展開時(shí)等于mHideBoundary * mBoundary在onTouch的ACTION_DOWN中變化 */ private int mBoundary; private int mUnfoldBoundary; private int mHideBoundary; /** * 阻尼值,越大越難拖動(dòng),呈線性趨勢(shì) */ private int mDumper; public PullDownDumperLayout(Context context, AttributeSet attrs) { super(context, attrs);// mScaledTouchSlop= ViewConfiguration.get(context).getScaledTouchSlop(); mHeadLayoutHideSpeed=-30; mHeadLayoutUnfoldSpeed=30; mSleepTime=10; mUnfoldRatio=0.6; mHideRatio=mUnfoldRatio; mDumper=2; mTopMarginOffset=-200; } /** * 布局開始設(shè)置每一個(gè)控件 * 在activity的onCreate執(zhí)行之后才會(huì)執(zhí)行 * 因此可以在onCreate中調(diào)用set方法設(shè)置參數(shù) */ @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); //只初始化一次 if(!mOnLayoutIsInit && changed) { //將第一個(gè)子元素作為頭部移出界面外 mHeadLayout = this.getChildAt(0); mHeadLayoutHeight=-mHeadLayout.getHeight(); mUnfoldBoundary=(int)(mUnfoldRatio*mHeadLayoutHeight);//計(jì)算觸發(fā)展開動(dòng)畫分界線 mHideBoundary=(int)(mHideRatio*mHeadLayoutHeight);//計(jì)算觸發(fā)隱藏動(dòng)畫分界線 mBoundary=mUnfoldBoundary;//觸發(fā)動(dòng)畫的分界線初始為mUnfoldBoundary mHeadLayoutHeight-=mTopMarginOffset;//頭部隱藏布局可見的部分 mHeadLayoutParams=(MarginLayoutParams) mHeadLayout.getLayoutParams(); mHeadLayoutParams.topMargin=mHeadLayoutHeight; mHeadLayout.setLayoutParams(mHeadLayoutParams); //TODO 設(shè)置手勢(shì)監(jiān)聽器,不能觸碰的控件需要添加android:clickable="true" getChildAt(1).setOnTouchListener(this); mHeadLayout.setOnTouchListener(this); //標(biāo)記已被初始化 mOnLayoutIsInit=true; } } /** * 屏幕觸摸操作監(jiān)聽器 * @return false: 注冊(cè)本監(jiān)聽器的控件將不會(huì)對(duì)事件做出響應(yīng),true則相反 */ @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: //根據(jù)此時(shí)處于完全展開或完全隱藏決定mBoundary的值,如果兩種情況都不滿足則不做改變 if(mHeadLayoutParams.topMargin==mHeadLayoutHeight) mBoundary=mUnfoldBoundary; else if(mHeadLayoutParams.topMargin==0) mBoundary=mHideBoundary;// mDownY=event.getRawY();//獲取按下的屏幕y坐標(biāo) mMoveY=event.getRawY(); mChangeHeadLayoutTopMargin=false;//false會(huì)打斷隱藏或展開頭部布局的動(dòng)畫 break; case MotionEvent.ACTION_MOVE: float currY=event.getRawY(); int vector=(int)(currY-mMoveY);//向量,用于判斷手勢(shì)的上滑和下滑 mMoveY=currY; //判斷是否為滑動(dòng) if(Math.abs(vector)==0){ return false; } //頭部完全隱藏時(shí)不再向上滑動(dòng) if (vector < 0 && mHeadLayoutParams.topMargin <= mHeadLayoutHeight) { return false; } //頭部完全展開時(shí)不再向下滑動(dòng) else if (vector > 0 && mHeadLayoutParams.topMargin >= 0) { return false; } //對(duì)增量進(jìn)行修正 int topMargin = mHeadLayoutParams.topMargin + (vector/mDumper); if(topMargin>0){ // 瞬間拉動(dòng)的距離超過了頭部高度,因?yàn)檫@一瞬間很短,這里采用直接賦值的方式 // 如需實(shí)現(xiàn)平滑過渡,要另開線程,并且監(jiān)聽到ACTION_DOWN時(shí)線程可被打斷 topMargin = 0; } else if(topMargin
看完上述內(nèi)容是否對(duì)您有幫助呢?如果還想對(duì)相關(guān)知識(shí)有進(jìn)一步的了解或閱讀更多相關(guān)文章,請(qǐng)關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝您對(duì)創(chuàng)新互聯(lián)的支持。