Android 仿豌豆荚应用列表进入详情效果

本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

效果图.gif

前两天买了个Android手机(ps:之前一直使用IPhone手机)打算给手机下载个应用市场,自己挺喜欢豌豆荚的,就下了个豌豆荚,在豌豆荚里下载App的时候发现它的列表进入详情效果挺好玩的,就想试试自己模仿一下。

思路

当初看到这个效果的时候就在想列表界面和详情界面是一个Activity + dialog 还是两个Activity,后来想了想详情界面数据挺多的应该不大可能是一个Activity,应该是一个列表Activity,一个详情Activity,那么针对这个设计就会有很多问题需要解决

  • 跳转的时候如何无缝实现点击的Item View 显示在详情Activity里是同一个位置呢?
  • 跳转成功以后如何还可以看到前面Activity的内容呢?
  • 如何让被点击的Item View慢慢的变化成详情页呢?
  • 详情View下拉出屏幕的时候如何退出详情Activity?
  • 下拉的时候如何动态的改变背景色透明度呢?

带着这些问题我们来一个一个分析解决

实现

  • 跳转的时候如何无缝实现点击的Item View 显示在详情Activity里是同一个位置呢?
    我们知道View在布局完成以后会有一个距离父类View顶部的属性top,那么在两个Activity中把View距离顶部的高度top设成一致就可以了,然后在跳转的时候去掉跳转动画就可以实现视觉上的无缝连接,下面我们来看看具体代码
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的高度,因为我是隐藏了状态栏,所有viewMarginTop 就是当前被点击的Item View距离状态栏顶部的高度;overridePendingTransition(0, 0)就是去掉跳转动画实现视觉无缝隙, 详情Activity如何显示会在下面分析。

  • 跳转成功以后如何还可以看到前面Activity的内容呢?
    其实就是把详情Activity背景设置成透明,并且把详情View的父类View背景都设置成透明就可以了,下面请看代码实现就是给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慢慢的变化成详情页呢?
    跳转到详情页以后要显示列表页被点击的Item View,设置它距离顶部的高度
mSVRootLl.setContentInitMarginTop(mViewMarginTop);
//方法
public void setContentInitMarginTop(int marginTop) {    
    mContentMarginTop = marginTop;    
    requestLayout();
 }

mViewMarginTop就是从列表界面传递过来的参数,mSVRootLl就是ScrollView下面的根LinearLayout,因为详情页面是可以滚动的,所以需要ScrollView,设置好高度以后,调用requestLayout方法发起布局,在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,这些参数的意义后面会分析到,这样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();           
           //内容布局顶部偏移量           
           int contentTopOffset = (int) (ratio * mContentTopOffsetNum);            
           //内容布局底部偏移量            
           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这个方法中根据ratio会计算4个偏移量,这4个偏移量有啥用呢?从动画中可以看到被点击的Item View 通过动画变成了一个详情View,这个变化的过程包括4部分:
1:Item View的上边距离顶部越来越近
2:Item View下边距离底部越来越近
3:Item View中的图片会慢慢居中
4:Item View中的图片会慢慢向下靠近
(如果不向下靠近,动画结束以后显示title布局,图片会有向下闪跃的问题)

那么这4部分移动的总距离乘以ratio就是动画执行过程中每次的偏移量,然后不断设置偏移量调用requestLayout方法发起布局来使View达到动画的效果;上面说到的mIsAnimation这个字段这个时候就是true了, mIsLayoutImageView也就是true了,只有执行动画的时候才会重新布局图片控件,动画结束以后会显示title布局和bottom布局

  • 详情View下拉出屏幕的时候如何退出详情Activity?
    从效果中可以看到下拉的高度超过一半就匀速向下滑动,滑出屏幕关闭Activity,小于一半就回弹到原来状态,这个功能需要用到事件分发原理,当ScrollView Y轴滚动为0并且是向下拉的时候就会触发View滑动这个事件,通过ACTION_DOWN记录初始点,ACTION_MOVE得到当前点,当前点减初始点得到滑动的距离(就是上面的mTouchMoveOffset变量),然后请求requestLayout方法发起布局调用onLayout方法刷新界面,手指松开的时候,判断滑动的距离是否超过一半,根据不同的状态通过动画改变mTouchMoveOffset的数值来刷新界面,下面来看看手势滑动和松开以后动画执行的代码
  @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();
}

动画执行完后调用回调方法关闭详情Activity

mSVRootLl.setOnCloseListener(new SVRootLinearLayout.OnCloseListener() {    
     @Override    
     public void onClose() {        
        finish();        
       overridePendingTransition(0, 0);    
    }
});
  • 下拉的时候如何动态的改变背景色透明度呢?

    通过代码可以看到在手势滑动的时候和动画的时候都调用了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设置的,通过调用onUpdate方法来改变背景的颜色透明度

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

结束语

到此为止,项目里的逻辑和代码都分析完了,通过文章我们可以得出在开发一个功能的时候,首先要有大致的思路,想如何去实现这个功能,然后把思路中的问题一一破解,最后串联起来就可以了,谢谢大家的阅读,有想看源码的可以移步github地址
https://github.com/chenpengfei88/WdjAppDetail
也欢迎大家Star,欢迎Follow我本人,谢谢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,743评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,296评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,285评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,485评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,581评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,821评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,960评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,719评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,186评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,516评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,650评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,936评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,757评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,991评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,370评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,527评论 2 349

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,781评论 25 707
  • 原文地址:http://www.android100.org/html/201606/06/241682.html...
    AFinalStone阅读 911评论 0 1
  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,366评论 0 17
  • 继续学习了窗体的高级应用 ,熟悉了我们平时使用软件的一些功能的建立比如快捷键还有对话框的建立。和富文本框的使用,下...
    刘博zero阅读 120评论 0 0
  • 先声明我不是李宇春的粉丝,窃取这首歌名为题只是因为偶然听了这首歌,加上前两天他给我上的“政治课”,有感而发。 “再...
    酒鬼阿辽阅读 227评论 0 0