Android 仿豌豆莢應用列表進入詳情效果

本篇文章已授權(quán)微信公眾號 guolin_blog (郭霖)獨家發(fā)布

效果圖.gif

前兩天買了個Android手機(ps:之前一直使用IPhone手機)打算給手機下載個應用市場,自己挺喜歡豌豆莢的,就下了個豌豆莢,在豌豆莢里下載App的時候發(fā)現(xiàn)它的列表進入詳情效果挺好玩的,就想試試自己模仿一下。

思路

當初看到這個效果的時候就在想列表界面和詳情界面是一個Activity + dialog 還是兩個Activity,后來想了想詳情界面數(shù)據(jù)挺多的應該不大可能是一個Activity,應該是一個列表Activity,一個詳情Activity,那么針對這個設計就會有很多問題需要解決

  • 跳轉(zhuǎn)的時候如何無縫實現(xiàn)點擊的Item View 顯示在詳情Activity里是同一個位置呢?
  • 跳轉(zhuǎn)成功以后如何還可以看到前面Activity的內(nèi)容呢?
  • 如何讓被點擊的Item View慢慢的變化成詳情頁呢?
  • 詳情View下拉出屏幕的時候如何退出詳情Activity?
  • 下拉的時候如何動態(tài)的改變背景色透明度呢?

帶著這些問題我們來一個一個分析解決

實現(xiàn)

  • 跳轉(zhuǎn)的時候如何無縫實現(xiàn)點擊的Item View 顯示在詳情Activity里是同一個位置呢?
    我們知道View在布局完成以后會有一個距離父類View頂部的屬性top,那么在兩個Activity中把View距離頂部的高度top設成一致就可以了,然后在跳轉(zhuǎn)的時候去掉跳轉(zhuǎn)動畫就可以實現(xiàn)視覺上的無縫連接,下面我們來看看具體代碼
int viewMarginTop = view.getTop() + getResources().getDimensionPixelOffset(R.dimen.bar_view_height);
Intent intent = new Intent(MainActivity.this, DetailActivity.class);
intent.putExtra("viewMarginTop", viewMarginTop);
intent.putExtra("imageId", (int) array[0]);
intent.putExtra("appName", (String) array[1]);startActivity(intent);
overridePendingTransition(0, 0);

view就是當前被點擊的Item View,view.getTop() 就是Item View距離RecyclerView頂部的高度,getResources().getDimensionPixelOffset(R.dimen.bar_view_height) 是RecyclerView上面Title View的高度,因為我是隱藏了狀態(tài)欄,所有viewMarginTop 就是當前被點擊的Item View距離狀態(tài)欄頂部的高度;overridePendingTransition(0, 0)就是去掉跳轉(zhuǎn)動畫實現(xiàn)視覺無縫隙, 詳情Activity如何顯示會在下面分析。

  • 跳轉(zhuǎn)成功以后如何還可以看到前面Activity的內(nèi)容呢?
    其實就是把詳情Activity背景設置成透明,并且把詳情View的父類View背景都設置成透明就可以了,下面請看代碼實現(xiàn)就是給Activity設置了一個透明的Theme
<style name="transparent" parent="Theme.AppCompat.Light.NoActionBar">  
     <item name="android:windowBackground">@android:color/transparent</item>
  <item name="android:windowIsTranslucent">true</item>
</style>
<activity android:name=".activity.DetailActivity" android:theme="@style/transparent"/>
  • 如何讓被點擊的Item View慢慢的變化成詳情頁呢?
    跳轉(zhuǎn)到詳情頁以后要顯示列表頁被點擊的Item View,設置它距離頂部的高度
mSVRootLl.setContentInitMarginTop(mViewMarginTop);
//方法
public void setContentInitMarginTop(int marginTop) {    
    mContentMarginTop = marginTop;    
    requestLayout();
 }

mViewMarginTop就是從列表界面?zhèn)鬟f過來的參數(shù),mSVRootLl就是ScrollView下面的根LinearLayout,因為詳情頁面是可以滾動的,所以需要ScrollView,設置好高度以后,調(diào)用requestLayout方法發(fā)起布局,在onLayout方法設置布局高度即可。

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {    
    super.onLayout(changed, l, t, r, b);    

    int contentTop = mContentMarginTop + mTouchMoveOffset;    
    mContentLL.layout(0, contentTop, mContentLlWidth, !mIsAnimation ? contentTop + mContentLlHeight : mInitBottom + mContentBottomOffset);   

    if(!mIsLayoutImageView) return;    
    int left = mMargin + mImageLeftOffset;    
    int top = mMargin + mImageTopOffset;    
    mIconImageView.layout(left, top, left + mIconImageViewWidth, top + mIconImageViewHeight);}

mContentMarginTop 就是剛才設置的高度,mTouchMoveOffset默認是0, mIsAnimation 默認是false,mIsLayoutImageView默認是false,這些參數(shù)的意義后面會分析到,這樣View的高度就和列表界面被點擊的Item View高度一樣了,接下來分析被點擊的Item View如何變化成詳情頁。

被點擊的Item View到詳情Activity以后就變成了一個LinearLayout布局,這個布局分為三部分: title布局,中間布局,bottom布局,默認title和bottom是隱藏的,所以默認情況下的效果就是列表界面被點擊Item View的效果,這個View顯示出來以后馬上通過一個動畫變成詳情界面,就是上面動畫完成以后的效果,下面我們來看看動畫的邏輯代碼

private void startAnimation() {    
    ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1).setDuration(400);    
    valueAnimator.setStartDelay(100);    
    valueAnimator.start();    
    valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {   
     
       @Override        
       public void onAnimationUpdate(ValueAnimator animation) {            
           float ratio = (float) animation.getAnimatedValue();           
           //內(nèi)容布局頂部偏移量           
           int contentTopOffset = (int) (ratio * mContentTopOffsetNum);            
           //內(nèi)容布局底部偏移量            
           int contentBottomOffset = (int) (ratio * mContentBottomOffsetNum);            
           //圖片左邊偏移量            
           int imageLeftOffset = (int) (ratio * mImageLeftOffsetNum);            
           //圖片上邊偏移量            
           int imageTopOffset =  (int) (ratio * mImageTopOffsetNum); 
     
           mSVRootLl.setAllViewOffset(mViewMarginTop - contentTopOffset, contentBottomOffset, imageLeftOffset, imageTopOffset);        
        }    
     });    
   valueAnimator.addListener(new AnimatorListenerAdapter() {        
        @Override        
        public void onAnimationEnd(Animator animation) {     
          super.onAnimationEnd(animation);   
          mSVRootLl.setAnimationStatus(false);        
          mBottomLl.setVisibility(View.VISIBLE);     
          mTitleLl.setVisibility(View.VISIBLE);       
       }   
   });
}

可以看到在onAnimationUpdate這個方法中根據(jù)ratio會計算4個偏移量,這4個偏移量有啥用呢?從動畫中可以看到被點擊的Item View 通過動畫變成了一個詳情View,這個變化的過程包括4部分:
1:Item View的上邊距離頂部越來越近
2:Item View下邊距離底部越來越近
3:Item View中的圖片會慢慢居中
4:Item View中的圖片會慢慢向下靠近
(如果不向下靠近,動畫結(jié)束以后顯示title布局,圖片會有向下閃躍的問題)

那么這4部分移動的總距離乘以ratio就是動畫執(zhí)行過程中每次的偏移量,然后不斷設置偏移量調(diào)用requestLayout方法發(fā)起布局來使View達到動畫的效果;上面說到的mIsAnimation這個字段這個時候就是true了, mIsLayoutImageView也就是true了,只有執(zhí)行動畫的時候才會重新布局圖片控件,動畫結(jié)束以后會顯示title布局和bottom布局

  • 詳情View下拉出屏幕的時候如何退出詳情Activity?
    從效果中可以看到下拉的高度超過一半就勻速向下滑動,滑出屏幕關閉Activity,小于一半就回彈到原來狀態(tài),這個功能需要用到事件分發(fā)原理,當ScrollView Y軸滾動為0并且是向下拉的時候就會觸發(fā)View滑動這個事件,通過ACTION_DOWN記錄初始點,ACTION_MOVE得到當前點,當前點減初始點得到滑動的距離(就是上面的mTouchMoveOffset變量),然后請求requestLayout方法發(fā)起布局調(diào)用onLayout方法刷新界面,手指松開的時候,判斷滑動的距離是否超過一半,根據(jù)不同的狀態(tài)通過動畫改變mTouchMoveOffset的數(shù)值來刷新界面,下面來看看手勢滑動和松開以后動畫執(zhí)行的代碼
  @Override
  public boolean onTouchEvent(MotionEvent event) {    
     boolean consumption = true;    
     switch (event.getAction()) {        
        case MotionEvent.ACTION_DOWN:            
            mInitY = event.getY();            
            getParent().requestDisallowInterceptTouchEvent(true);            
            break;        
        case MotionEvent.ACTION_MOVE:           
            float moveY = event.getY();            
            float yOffset = moveY - mInitY;           
            //拖動            
            if((mParentScrollView.getScrollY() <= 0 && moveY >= mInitY) || mIsDrag) {     
                setTouchMoveOffset(yOffset);                
                mIsDrag = true;                
                consumption = true;            
            } else {                
                getParent().requestDisallowInterceptTouchEvent(false);       
                consumption = false;            
            }            
            break;        
        case MotionEvent.ACTION_UP:            
            mIsDrag = false;            
            boolean isUp = false;            
            int animationMoveOffset;           
            if(mContentLL.getTop() <= mCenterVisibleViewHeight / 2 + mTitleViewHeight) {                
                 animationMoveOffset = mTouchMoveOffset;                
                 isUp = true;            
            } else {                
                 animationMoveOffset = mParentScrollView.getHeight() - mContentLL.getTop();    
            }              
            startAnimation(animationMoveOffset, isUp, mTouchMoveOffset);         
            break;    
     }   
         return consumption;
}
public void startAnimation(final int moveOffset, final boolean isUp, final int currentMoveOffset) {    
   int duration = moveOffset / mTouchSlop * 10;   
   if(duration <= 0) duration = 300;    
   ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1).setDuration(duration);    
   valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {        
      @Override        
      public void onAnimationUpdate(ValueAnimator animation) {            
          float ratio = (float) animation.getAnimatedValue();            
          if(isUp)                
           mTouchMoveOffset = (int) (moveOffset * (1 - ratio));            
          else                
           mTouchMoveOffset = currentMoveOffset + (int) (moveOffset * ratio);      
           requestLayout();            
           updateBgColor(mTouchMoveOffset);        
      }    
   });    
   valueAnimator.addListener(new AnimatorListenerAdapter() {        
     @Override        
     public void onAnimationEnd(Animator animation) {     
         super.onAnimationEnd(animation);            
         if(!isUp && mOnCloseListener != null) mOnCloseListener.onClose();        
     }    
 });    
   valueAnimator.start();
}

動畫執(zhí)行完后調(diào)用回調(diào)方法關閉詳情Activity

mSVRootLl.setOnCloseListener(new SVRootLinearLayout.OnCloseListener() {    
     @Override    
     public void onClose() {        
        finish();        
       overridePendingTransition(0, 0);    
    }
});
  • 下拉的時候如何動態(tài)的改變背景色透明度呢?

    通過代碼可以看到在手勢滑動的時候和動畫的時候都調(diào)用了updateBgColor方法

public void updateBgColor(int offset) {    
   if(mOnUpdateBgColorListener != null) {        
      float ratio = BigDecimalUtils.divide(offset, mCenterVisibleViewHeight);        
    if(ratio > 1) ratio = 1;        
    if(ratio < 0) ratio = 0;        
    mOnUpdateBgColorListener.onUpdate(ratio);    
   }
}

這個方法里的mOnUpdateBgColorListener是詳情Activity設置的,通過調(diào)用onUpdate方法來改變背景的顏色透明度

mSVRootLl.setOnUpdateBgColorListener(new SVRootLinearLayout.OnUpdateBgColorListener() {    
    @Override    
    public void onUpdate(float ratio) {        
      mRootCDrawable.setAlpha((int) (mColorInitAlpha - mColorInitAlpha * ratio));    
    }
});

結(jié)束語

到此為止,項目里的邏輯和代碼都分析完了,通過文章我們可以得出在開發(fā)一個功能的時候,首先要有大致的思路,想如何去實現(xiàn)這個功能,然后把思路中的問題一一破解,最后串聯(lián)起來就可以了,謝謝大家的閱讀,有想看源碼的可以移步github地址
https://github.com/chenpengfei88/WdjAppDetail
也歡迎大家Star,歡迎Follow我本人,謝謝。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,556評論 25 708
  • 原文地址:http://www.android100.org/html/201606/06/241682.html...
    AFinalStone閱讀 998評論 0 1
  • ¥開啟¥ 【iAPP實現(xiàn)進入界面執(zhí)行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程,因...
    小菜c閱讀 6,554評論 0 17
  • 繼續(xù)學習了窗體的高級應用 ,熟悉了我們平時使用軟件的一些功能的建立比如快捷鍵還有對話框的建立。和富文本框的使用,下...
    劉博zero閱讀 132評論 0 0
  • 先聲明我不是李宇春的粉絲,竊取這首歌名為題只是因為偶然聽了這首歌,加上前兩天他給我上的“政治課”,有感而發(fā)。 “再...
    酒鬼阿遼閱讀 248評論 0 0