美文网首页自定义控件实用控件Android研究院
Android 仿豌豆荚应用列表进入详情效果

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

作者: 伪文艺大叔 | 来源:发表于2016-11-25 10:08 被阅读1619次

    本篇文章已授权微信公众号 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我本人,谢谢。

    相关文章

      网友评论

      • 23c99f3f2dba:你的代码在这里加个判断感觉更好
        :if (mTouchMoveOffset > 0 || animationMoveOffset > 0) {
        startAnimation(animationMoveOffset, isUp, mTouchMoveOffset);
        }
        23c99f3f2dba: @伪文艺大叔 嗯
        伪文艺大叔:两个都要大于0

      本文标题:Android 仿豌豆荚应用列表进入详情效果

      本文链接:https://www.haomeiwen.com/subject/aivbpttx.html