中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Android中怎么實現下拉阻尼效果

發布時間:2021-08-07 15:05:35 來源:億速云 閱讀:157 作者:Leah 欄目:編程語言

Android中怎么實現下拉阻尼效果,很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。

原理

這種效果是通過自定義控件的方式來實現的,我自定義了一個控件類型,這個自定義控件(PullDownDumperLayout)繼承自線性布局(LinearLayout)

用戶可以下拉彈出的那個視圖,例如微信的小程序列表,開發者只是將這個視圖移出了父元素之外,所以不可見,我們暫且稱之為隱藏頭部,只有下拉到一定程度才會彈出,而主體,例如微信的聯系人列表,則是可見的,布局見下圖。

實現這個效果需要我們做三件工作:

1.隱藏作為頭部的控件2.監聽用戶對屏幕的操作事件3.實現下拉回彈的動畫效果

我們這個自定義控件會自動獲取內部第一個子元素充當頭部,其余的元素則是充當可見的主體(詳見代碼中的注釋)。

基本的布局原理差不多就這樣了,但是我們還需要讓自定義控件監聽用戶的手勢操作,例如上下滑動等。這里我和靈感來源的那篇博客一樣,讓自定義控件實現View.OnTouchListener接口,實現內部的onTouch方法可以監聽來自屏幕的所有觸摸操作。代碼中我讓頭部和第二個子元素(可見的主體)注冊了這個監聽器,這是為了方便讀者理解,讀者可根據自己的需求進行修改。

注意,對于不能監聽屏幕觸摸事件的控件需要添加:

android:clickable="true"

至此,我們已經可以進行布局和監聽用戶手勢了,但是還需要實現一個頭部展開和隱藏的動畫效果。當用戶將隱藏頭部下拉或上滑到一定高度時,這個效果就會被觸發,這需要依賴上面所述的onTouch方法。動畫效果的實現需要另開一個線程進行操作,線程的啟動方式我們可以采用繼承AsyncTask類來實現。

除此之外,我們可能會多次復用這個控件,所以在自定義控件類的最后還需要一些調整參數的set方法。

這里提個醒,在接下來的代碼中,我們的自定義控件因為繼承自LinearLayout,里面需要重寫onLayout方法,而onLayout方法顧名思義就是布局,這個方法在Activity中的onCreate方法執行之后才會被調用,所以我們可以在ActivityonCreate方法中利用findViewById獲取實例,調用上面提到的set方法進行參數的初始化。

LinearLayout中不止onLayout一個方法,詳細解析請讀者移步其他關于XML標簽加載過程的文章,這里不做贅述。

代碼

PullDownDumperLayout .java:

public class PullDownDumperLayout extends LinearLayout implements View.OnTouchListener {  /**   * 取布局中的第一個子元素為下拉隱藏頭部   */  private View mHeadLayout;  /**   * 隱藏頭部布局的高的負值   */  private int mHeadLayoutHeight;  /**   * 隱藏頭部的布局參數   */  private MarginLayoutParams mHeadLayoutParams;  /**   * 判斷是否為第一次初始化,第一次初始化需要把headView移出界面外   */  private boolean mOnLayoutIsInit=false;  /**   * 移動時,前一個坐標   */  private float mMoveY;  /**   * 如果為false,會退出頭部展開或隱藏動畫   */  private boolean mChangeHeadLayoutTopMargin;  /**   * 觸發動畫的分界線,由mRatio計算得到   */  private int mBoundary;  /**   * 頭部布局的隱藏和展開速度,以及單次執行時間   */  private int mHeadLayoutHideSpeed;  private int mHeadLayoutUnfoldSpeed;  private long mSleepTime;  /**   * 觸發動畫的分界線,頭部布局上半部分和整體高度的比例   */  private double mRatio;  public PullDownDumperLayout(Context context, AttributeSet attrs) {    super(context, attrs);    //初始化參數,根據自己的需求調整    mHeadLayoutHideSpeed=-20;    mHeadLayoutUnfoldSpeed=20;    mSleepTime=10;    mRatio=0.5;  }  /**   * 布局開始設置每一個控件   * 在activity的onCreate執行之后才會執行   * 因此可以在onCreate中調用set方法設置參數   */  @Override  protected void onLayout(boolean changed, int l, int t, int r, int b) {    super.onLayout(changed, l, t, r, b);    if(!mOnLayoutIsInit && changed) {      //將第一個子元素作為頭部移出界面外      mHeadLayout = this.getChildAt(0);      mHeadLayoutHeight=-mHeadLayout.getHeight();      mBoundary=(int)(mRatio*mHeadLayoutHeight);//計算觸發動畫分界線      mHeadLayoutParams=(MarginLayoutParams) mHeadLayout.getLayoutParams();      mHeadLayoutParams.topMargin=mHeadLayoutHeight;      mHeadLayout.setLayoutParams(mHeadLayoutParams);      //TODO 設置手勢監聽器,不能觸碰的控件需要添加android:clickable="true"      getChildAt(1).setOnTouchListener(this);      mHeadLayout.setOnTouchListener(this);      //標記已被初始化      mOnLayoutIsInit=true;    }  }  /**   * 屏幕觸摸操作監聽器   * @return false則注冊本監聽器的控件將不會對事件做出響應,true則相反   */  @Override  public boolean onTouch(View v, MotionEvent event) {    switch (event.getAction()) {      case MotionEvent.ACTION_DOWN:        mMoveY=event.getRawY();//捕獲按下時的坐標,初始化mMoveY        mChangeHeadLayoutTopMargin=false;        break;      case MotionEvent.ACTION_MOVE:        float currY=event.getRawY();        int vector=(int)(currY-mMoveY);//向量,用于判斷手勢的上滑和下滑        mMoveY=currY;        //判斷是否為滑動        if(Math.abs(vector)==0){          return false;        }        //頭部完全隱藏時不再向上滑動        if (vector < 0 && mHeadLayoutParams.topMargin <= mHeadLayoutHeight) {          return false;        }        //頭部完全展開時不再向下滑動        if (vector > 0 && mHeadLayoutParams.topMargin >= 0) {          return false;        }        //對增量進行修正,對滑動距離進行減半        int topMargin = mHeadLayoutParams.topMargin + (vector/2);//阻尼值        if(topMargin>0){          // 瞬間拉動的距離超過了頭部高度,因為這一瞬間很短,這里采用直接賦值的方式          // 如需平滑過渡,要另開線程,并且監聽到ACTION_DOWN時線程可被打斷          topMargin = 0;        }        else if(topMargin<mHeadLayoutHeight){          // 瞬間拉動的距離超過了頭部高度,因為這一瞬間很短,這里采用直接賦值的方式          // 如需平滑過渡,要另開線程,并且監聽ACTION_DOWN時線程可被打斷          topMargin = mHeadLayoutHeight;        }        //用戶對屏幕的滑動將會改變控件的TopMargin        mHeadLayoutParams.topMargin = topMargin ;        mHeadLayout.setLayoutParams(mHeadLayoutParams);        break;      default:        //TODO 出現其他觸碰事件,如MotionEvent.ACTION_UP時,根據閾值判斷此時頭部應該彈出還是隱藏        mChangeHeadLayoutTopMargin=true;        if(mHeadLayoutParams.topMargin<=mBoundary){          //隱藏          new MoveHeaderTask().execute(true);        }        else{          //展開          new MoveHeaderTask().execute(false);        }        break;    }    return false;  }  /**   * 新線程,隱藏或者展開頭部布局,線程可被ACTION_DOWN打斷   */  class MoveHeaderTask extends AsyncTask<Boolean, Integer, Integer> {    /**     *     * @param opt true為隱藏動畫,false為展開動畫     * @return     */    @Override    protected Integer doInBackground(Boolean... opt) {      int topMargin=mHeadLayoutParams.topMargin;      //true為隱藏,false為展開      int speed=(opt[0])?mHeadLayoutHideSpeed:mHeadLayoutUnfoldSpeed;      while(mChangeHeadLayoutTopMargin){        topMargin += speed;        if (topMargin <= mHeadLayoutHeight||topMargin>=0) {          topMargin=(opt[0])?mHeadLayoutHeight:0;          publishProgress(topMargin);          break;        }        publishProgress(topMargin);        sleep(mSleepTime);      }      return null;    }    //調用publishProgress后會執行    @Override    protected void onProgressUpdate(Integer... topMargin) {      mHeadLayoutParams.topMargin=topMargin[0];      mHeadLayout.setLayoutParams(mHeadLayoutParams);    }  }  //調整參數  public void setHeadLayoutHideSpeed(int speed){    this.mHeadLayoutHideSpeed=speed;  }  public void setHeadLayoutUnfoldSpeed(int speed){    this.mHeadLayoutUnfoldSpeed=speed;  }  public void setSleepTime(long time){    this.mSleepTime=time;  }  public void setRatio(double ratio){    this.mRatio=ratio;  }}

activity_main.xml:

<?xml version="1.0" encoding="utf-8"?><android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"  xmlns:app="http://schemas.android.com/apk/res-auto"  xmlns:tools="http://schemas.android.com/tools"  android:layout_width="match_parent"  android:layout_height="match_parent"  tools:context=".MainActivity">  <com.example.pulldowndumpertest.PullDownDumperLayout    android:tag="記得將這個標簽修改為自己的包名"    android:id="@+id/PullDownDumper"    android:layout_width="900px"    android:layout_height="1920px"    app:layout_constraintLeft_toLeftOf="parent"    app:layout_constraintRight_toRightOf="parent"    app:layout_constraintTop_toTopOf="parent"    android:background="@null"    android:orientation="vertical">    <LinearLayout      android:layout_width="match_parent"      android:layout_height="500px"      android:orientation="vertical"      android:background="@color/colorPrimary"      android:clickable="true">      <TextView        android:layout_width="match_parent"        android:layout_height="match_parent"        android:text="隱藏頭部"        android:textSize="100px"        android:gravity="center"        android:textColor="#FFFFFF"        android:background="@null"/>    </LinearLayout>    <LinearLayout      android:layout_width="match_parent"      android:layout_height="1700px"      android:background="@color/colorPrimaryDark"      android:clickable="true">      <TextView        android:layout_width="match_parent"        android:layout_height="match_parent"        android:text="可見主體"        android:textSize="100px"        android:gravity="center"        android:textColor="#FFFFFF"        android:background="@null"/>    </LinearLayout>  </com.example.pulldowndumpertest.PullDownDumperLayout></android.support.constraint.ConstraintLayout>

MainActivity.java:

public class MainActivity extends AppCompatActivity {  @Override  protected void onCreate(Bundle savedInstanceState) {    super.onCreate(savedInstanceState);    setContentView(R.layout.activity_main);    //TODO 讀者可在這里初始化參數    PullDownDumperLayout pddl=findViewById(R.id.PullDownDumper);  }}

下面是筆者正在使用的自定義控件,比上述的控件多了一個效果:

頭部處于隱藏或展開的不同狀態時,觸發動畫效果的分界線可以隨狀態不同而改變。

還是拿最新版的微信小程序入口來講,用戶在下拉時,小程序界面會占用整個屏幕,如果觸發動畫的分界線太低,這樣導致的結果是用戶可能無法通過上滑重新返回聯系人列表,但由于微信沒有對滑動距離進行減半處理,所以不存在上述問題,可能是出于防止誤觸的原因,從小程序界面返回聯系人列表的方式改用點擊底部的一個按鈕。而我的控件可以通過改變觸發動畫效果的分界線來解決這一問題,感興趣的讀者可以研究一下。

public class PullDownDumperLayout extends LinearLayout implements View.OnTouchListener {  /**   * 取布局中的第一個子元素為下拉隱藏頭部   */  private View mHeadLayout;  /**   * 隱藏頭部布局的高的負值   */  private int mHeadLayoutHeight;  /**   * 隱藏頭部的布局參數   */  private MarginLayoutParams mHeadLayoutParams;  /**   * 判斷是否為第一次初始化,第一次初始化需要把headView移出界面外   */  private boolean mOnLayoutIsInit=false;  /**   * 從配置獲取的滾動判斷閾值,為兩點間的距離,超過此閾值判斷為滾動   *///  private int mScaledTouchSlop;  /**   * 按下時的y軸坐標   *///  private float mDownY;  /**   * 移動時,前一個坐標   */  private float mMoveY;  /**   * 如果為false,會退出頭部展開或隱藏動畫   */  private boolean mChangeHeadLayoutTopMargin;  /**   * 頭部布局的隱藏和展開速度,以及單次執行時間   */  private int mHeadLayoutHideSpeed;  private int mHeadLayoutUnfoldSpeed;  private long mSleepTime;  /**   * 初始化頭部布局的偏移值,數值越大,頭部可見部分越多,預設值為0,即初始時頭部完全不可見   */  private int mTopMarginOffset;  /**   * 觸發動畫的分界線,頭部布局上半部分和整體高度的比例   */  private double mUnfoldRatio;  private double mHideRatio;  /**   * 觸發動畫的分界線,初始值由mRatio計算得到   * 頭部處于隱藏時等于mUnfoldBoundary   * 頭部處于展開時等于mHideBoundary   * mBoundary在onTouch的ACTION_DOWN中變化   */  private int mBoundary;  private int mUnfoldBoundary;  private int mHideBoundary;  /**   * 阻尼值,越大越難拖動,呈線性趨勢   */  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;  }  /**   * 布局開始設置每一個控件   * 在activity的onCreate執行之后才會執行   * 因此可以在onCreate中調用set方法設置參數   */  @Override  protected void onLayout(boolean changed, int l, int t, int r, int b) {    super.onLayout(changed, l, t, r, b);    //只初始化一次    if(!mOnLayoutIsInit && changed) {      //將第一個子元素作為頭部移出界面外      mHeadLayout = this.getChildAt(0);      mHeadLayoutHeight=-mHeadLayout.getHeight();      mUnfoldBoundary=(int)(mUnfoldRatio*mHeadLayoutHeight);//計算觸發展開動畫分界線      mHideBoundary=(int)(mHideRatio*mHeadLayoutHeight);//計算觸發隱藏動畫分界線      mBoundary=mUnfoldBoundary;//觸發動畫的分界線初始為mUnfoldBoundary      mHeadLayoutHeight-=mTopMarginOffset;//頭部隱藏布局可見的部分      mHeadLayoutParams=(MarginLayoutParams) mHeadLayout.getLayoutParams();      mHeadLayoutParams.topMargin=mHeadLayoutHeight;      mHeadLayout.setLayoutParams(mHeadLayoutParams);      //TODO 設置手勢監聽器,不能觸碰的控件需要添加android:clickable="true"      getChildAt(1).setOnTouchListener(this);      mHeadLayout.setOnTouchListener(this);      //標記已被初始化      mOnLayoutIsInit=true;    }  }  /**   * 屏幕觸摸操作監聽器   * @return false: 注冊本監聽器的控件將不會對事件做出響應,true則相反   */  @Override  public boolean onTouch(View v, MotionEvent event) {    switch (event.getAction()) {      case MotionEvent.ACTION_DOWN:        //根據此時處于完全展開或完全隱藏決定mBoundary的值,如果兩種情況都不滿足則不做改變        if(mHeadLayoutParams.topMargin==mHeadLayoutHeight)          mBoundary=mUnfoldBoundary;        else if(mHeadLayoutParams.topMargin==0)          mBoundary=mHideBoundary;//        mDownY=event.getRawY();//獲取按下的屏幕y坐標        mMoveY=event.getRawY();        mChangeHeadLayoutTopMargin=false;//false會打斷隱藏或展開頭部布局的動畫        break;      case MotionEvent.ACTION_MOVE:        float currY=event.getRawY();        int vector=(int)(currY-mMoveY);//向量,用于判斷手勢的上滑和下滑        mMoveY=currY;        //判斷是否為滑動        if(Math.abs(vector)==0){          return false;        }        //頭部完全隱藏時不再向上滑動        if (vector < 0 && mHeadLayoutParams.topMargin <= mHeadLayoutHeight) {          return false;        }        //頭部完全展開時不再向下滑動        else if (vector > 0 && mHeadLayoutParams.topMargin >= 0) {          return false;        }        //對增量進行修正        int topMargin = mHeadLayoutParams.topMargin + (vector/mDumper);        if(topMargin>0){          // 瞬間拉動的距離超過了頭部高度,因為這一瞬間很短,這里采用直接賦值的方式          // 如需實現平滑過渡,要另開線程,并且監聽到ACTION_DOWN時線程可被打斷          topMargin = 0;        }        else if(topMargin<mHeadLayoutHeight){          // 瞬間拉動的距離超過了頭部高度,因為這一瞬間很短,這里采用直接賦值的方式          // 如需實現平滑過渡,要另開線程,并且監聽ACTION_DOWN時線程可被打斷          topMargin = mHeadLayoutHeight;        }        //使參數生效        mHeadLayoutParams.topMargin = topMargin ;        mHeadLayout.setLayoutParams(mHeadLayoutParams);        break;      default:        //出現其他觸碰事件,如MotionEvent.ACTION_UP時,根據閾值mBoundary判斷此時頭部應該彈出還是隱藏        mChangeHeadLayoutTopMargin=true;//允許執行動畫        if(mHeadLayoutParams.topMargin<=mBoundary){          //隱藏          new MoveHeaderTask().execute(true);        }        else{          //展開          new MoveHeaderTask().execute(false);        }        break;    }    return false;  }  /**   * 新線程,隱藏或者展開頭部布局,線程可被ACTION_DOWN打斷   */  private class MoveHeaderTask extends AsyncTask<Boolean, Integer, Integer> {    /**     *     * @param opt true為隱藏動畫,false為展開動畫     * @return     */    @Override    protected Integer doInBackground(Boolean... opt) {      int topMargin=mHeadLayoutParams.topMargin;      //true為隱藏,false為展開      int speed=(opt[0])?mHeadLayoutHideSpeed:mHeadLayoutUnfoldSpeed;      while(mChangeHeadLayoutTopMargin){        topMargin += speed;        if (topMargin <= mHeadLayoutHeight||topMargin>=0) {          topMargin=(opt[0])?mHeadLayoutHeight:0;          publishProgress(topMargin);          break;        }        publishProgress(topMargin);        sleep(mSleepTime);      }      return null;    }    //調用publishProgress后會執行    @Override    protected void onProgressUpdate(Integer... topMargin) {      mHeadLayoutParams.topMargin=topMargin[0];      mHeadLayout.setLayoutParams(mHeadLayoutParams);    }  }  //調整參數  public void setHeadLayoutHideSpeed(int speed){    this.mHeadLayoutHideSpeed=speed;  }  public void setHeadLayoutUnfoldSpeed(int speed){    this.mHeadLayoutUnfoldSpeed=speed;  }  public void setSleepTime(long time){    this.mSleepTime=time;  }  public void setDumper(int dumper){    this.mDumper=dumper;  }  public void setTopMarginOffset(int offset){    this.mTopMarginOffset=-offset;  }  /**   * 頭部處于隱藏狀態時,觸發展開動畫的分界線   * @param ratio 頭部布局上部分與下部分的分界線   */  public void setUnfoldRatio(double ratio){    this.mUnfoldRatio=ratio;  }  /**   * 頭部處于展開狀態時,觸發隱藏動畫的分界線   * @param ratio 頭部布局上部分與下部分的分界線   */  public void setHideRatio(double ratio){    this.mHideRatio=ratio;  }}```

看完上述內容是否對您有幫助呢?如果還想對相關知識有進一步的了解或閱讀更多相關文章,請關注億速云行業資訊頻道,感謝您對億速云的支持。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

开封市| 武隆县| 治多县| 体育| 舟山市| 图木舒克市| 五台县| 信阳市| 陆良县| 佳木斯市| 新疆| 保山市| 图木舒克市| 南阳市| 松潘县| 米泉市| 盐城市| 潜山县| 鄂伦春自治旗| 玉溪市| 南丹县| 长白| 石泉县| 霍林郭勒市| 龙州县| 宁南县| 济源市| 油尖旺区| 临澧县| 化州市| 侯马市| 广昌县| 东阿县| 台中县| 马龙县| 东丰县| 建瓯市| 泰安市| 喜德县| 工布江达县| 徐汇区|