Android仿“知乎”隐藏标题栏、回答详情页动画效果

2014已经远去,2015年的目标很简单,就是继续熟悉Android的上层API,虽然偶尔会为了某个问题去研究下FrameWork的代码,但是对于我们这种新手来说,只有对上层的API用的熟练了,才能更好的往下研究原理。所以,今年的任务就是继续学习和研究Android的上层API的使用,顺便写一篇毕业论文,然后毕个业。

    OK,从这篇开始,咱们就开始【凯子哥带你夯实应用层】系列,如果你有想要实现的界面效果,或者是有一些开发中的疑问,请私信我,如果我觉得比较好的话,会自己实现一下,然后写blog和大家分享实现思路。

    废话不多说,咱们第一篇文章就是模仿“知乎”的回答详情页的动画效果,先上个原版的效果图,咱们就是要做出这个效果


 

    在实现之前,我们先根据上面的动画效果,研究下需求,因为gif帧数有限,所以不是很连贯,推荐你直接下载一个知乎,找到这个界面自己玩玩

☞当文章往上移动到一定位置之后,最上面的标题栏Bar和问题布局Title是会隐藏的,回答者Author布局不会隐藏

☞当文章往下移动移动到一定位置之后,原先隐藏的标题栏Bar和问题布局Title会下降显示

☞当文章往上移动的时候,下部隐藏的Tools布局会上升显示

☞当文章往下移动的时候,如果Tools布局是显示的,则隐藏

☞当标题栏Bar和问题布局Title下降显示的时候,Title是从Bar的下面出来的,有个遮挡的效果

☞当快速滑动内容到达底部的时候,隐藏的Tools会显示出来

☞当快速滑动内容到顶部的时候,隐藏的Bar和Title也会显示出来

 

    不分析不知道,这样一个简单地效果,经过分析需要完成不少东西呢,那么下面根据要实现的需求,咱们分析一下解决方案。

    在做这种仿界面之前,我们可以使用ADT带的View Hierarchy工具看一下“知乎”原生是怎么实现的


 

    从右边的分析图可以看出,知乎的这个界面,内容用的WebView,这很正常,因为用户的回答里面格式比较复杂,用WebView是最好的解决方案,而标题栏是一个VIew,是ActionBar还是自定义View呢,不得而知,下面是就是一个LinearLayout包了4个ToggleButton,布局很简单,我们没有WebView,所以使用ScrollView代替,上面的布局直接ImageView了,设置个src,模拟一个布局。

    其实布局很简单,咱们一个效果一个效果的来实现。

    首先是下面的Tools如何显示和隐藏呢?当然是用动画了!什么动画呢?能实现的有属性动画和帧动画,属性动画能够真实的改变View的属性,帧动画只是视觉上移动了,View的实际属性并不改变,这两种都可以,我们这里使用属性动画

 

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
 
 
  1. /** 
  2.      * 显示工具栏 
  3.      */  
  4.     private void showTools() {  
  5.   
  6.         ObjectAnimator anim = ObjectAnimator.ofFloat(img_tools, "y", img_tools.getY(),  
  7.                 img_tools.getY() - img_tools.getHeight());  
  8.         anim.setDuration(TIME_ANIMATION);  
  9.         anim.start();  
  10.   
  11.         isToolsHide = false;  
  12.     }  
  13.   
  14.     /** 
  15.      * 隐藏工具栏 
  16.      */  
  17.     private void hideTools() {  
  18.   
  19.         ObjectAnimator anim = ObjectAnimator.ofFloat(img_tools, "y", img_tools.getY(),  
  20.                 img_tools.getY() + img_tools.getHeight());  
  21.         anim.setDuration(TIME_ANIMATION);  
  22.         anim.start();  
  23.   
  24.         isToolsHide = true;  
  25.   
  26.     }  


    那么什么时候调用呢?从上面的需求分析中,我们知道,用户手指下拉的时候,Tools显示,反之隐藏,那么我们就可以监听ScrollView的onTouch,判断手指方向,实现动画效果的调用

 

 

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
 
 
  1. mScroller.setOnTouchListener(new View.OnTouchListener() {  
  2.             @Override  
  3.             public boolean onTouch(View v, MotionEvent event) {  
  4.   
  5.   
  6.                 switch (event.getAction()) {  
  7.   
  8.                     case MotionEvent.ACTION_DOWN:  
  9.                         lastY = event.getY();  
  10.                         break;  
  11.                     case MotionEvent.ACTION_MOVE:  
  12.   
  13.                         float disY = event.getY() - lastY;  
  14.   
  15.                         //垂直方向滑动  
  16.                         if (Math.abs(disY) > viewSlop) {  
  17.                             //是否向上滑动  
  18.                             isUpSlide = disY < 0;  
  19.   
  20.                             //实现底部tools的显示与隐藏  
  21.                             if (isUpSlide) {  
  22.                                 if (!isToolsHide)  
  23.                                     hideTools();  
  24.                             } else {  
  25.                                 if (isToolsHide)  
  26.                                     showTools();  
  27.                             }  
  28.                         }  
  29.   
  30.                         break;  
  31.                 }  
  32.   
  33.                 return false;  
  34.             }  
  35.         });  


     用变量isToolsHide放置代码重复调用。

 

 

    下面的Tools的问题解决了,我们再看一下上面的布局动画如何来实现。上面的思路和下面一样,也是通过属性动画,完成位置的移动,移动的布局有Bar、Title和根布局。为什么答题人布局Author不移动呢?因为根布局必须移动,否则就会挡住下面的文字内容,根布局的移动会让子布局都跟着移动,所以只移动根布局即可。

    对了,为了更大范围的现实文本,“知乎”的WebView是占据整个布局的,其他布局都是在根布局FrameLayout里面,所以,在首次加载的时候,下面的文本在开头需要留出一定的间隔,防止被遮挡,当上面的布局隐藏之后,就没有问题了。

    在简单分析之后,我再给出实现的布局的代码

 

[html]  view plain copy 在CODE上查看代码片 派生到我的代码片
 
 
  1. <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.              android:layout_width="match_parent"  
  3.              android:layout_height="match_parent"  
  4.              android:background="@android:color/white"  
  5.     >  
  6.   
  7.   
  8.     <com.socks.zhihudetail.MyScrollView  
  9.         android:id="@+id/scroller"  
  10.         android:layout_width="match_parent"  
  11.         android:layout_height="wrap_content"  
  12.         >  
  13.   
  14.         <TextView  
  15.             android:layout_width="match_parent"  
  16.             android:layout_height="match_parent"  
  17.             android:textSize="16sp"  
  18.             android:textColor="@android:color/black"  
  19.             android:text="@string/hello_world"/>  
  20.   
  21.     </com.socks.zhihudetail.MyScrollView>  
  22.   
  23.   
  24.     <FrameLayout  
  25.         android:id="@+id/ll_top"  
  26.         android:layout_width="match_parent"  
  27.         android:layout_height="wrap_content"  
  28.         android:background="@android:color/white"  
  29.         android:orientation="vertical"  
  30.         android:layout_gravity="top">  
  31.   
  32.         <ImageView  
  33.             android:id="@+id/img_author"  
  34.             android:layout_width="match_parent"  
  35.             android:layout_height="80dp"  
  36.             android:scaleType="fitXY"  
  37.             android:src="@drawable/bg_author"/>  
  38.   
  39.         <TextView  
  40.             android:id="@+id/tv_title"  
  41.             android:layout_width="match_parent"  
  42.             android:layout_height="wrap_content"  
  43.             android:layout_marginTop="55dp"  
  44.             android:text="为什么美国有那么多肌肉极其强大的肌肉男?"  
  45.             android:textSize="18sp"  
  46.             android:background="#DBDBDB"  
  47.             android:gravity="center|left"  
  48.             android:paddingLeft="15dp"  
  49.             android:paddingRight="15dp"  
  50.             android:paddingTop="5dp"  
  51.             android:paddingBottom="5dp"  
  52.             android:textColor="@android:color/darker_gray"  
  53.             />  
  54.   
  55.         <ImageView  
  56.             android:id="@+id/img_bar"  
  57.             android:layout_width="match_parent"  
  58.             android:layout_height="55dp"  
  59.             android:scaleType="fitXY"  
  60.             android:src="@drawable/bg_actionbar"/>  
  61.   
  62.     </FrameLayout>  
  63.   
  64.     <ImageView  
  65.         android:id="@+id/img_tools"  
  66.         android:layout_width="match_parent"  
  67.         android:layout_height="wrap_content"  
  68.         android:scaleType="fitXY"  
  69.         android:layout_gravity="bottom"  
  70.         android:src="@drawable/bg_bottom"/>  
  71.   
  72.   
  73. </FrameLayout>  


    效果图如下,文本留了一些空行,保证不被遮挡。

 


 

    有的同学看了上面的效果图可能会疑惑,这里为什么没有答题人的布局呢?

    其实是这样的,为了模拟上部的布局显示时,Title从Bar下面出现的效果,所以特意这样设计的。我试过用linearLayout实现,效果也是可以实现的,但是当Title往下移动显示的时候,会覆盖在Bar上面,这也很好理解,LinearLayout没有层次顺序,所以会遮挡。我试过View.bringToFront(),试图把Bar的布局提高层次,但是这样会导致布局的紊乱,在首次加载的时候,Bar会显示在最下面,是因为提高层次之后,Bar的布局重新计算,所以不按照LinearLayout的布局规则来了。无奈之下,换成了Framelayout,但是又出现了问题,Bar的高度可以设置,但是Title的高度会随着文本的增加而改变,这样一来,最下面Author的布局的位置就不能设置了,因为不知道距离上面多远,所以我们只能在代码里面动态的计算Bar和Title的高度,然后在界面加载的时候,动态的给Author的布局设置MargenTop,保证位置的正确。

    因为在onCreate里面,还没有开始View的绘制,所以得不到控件的真实高度,我们可以用下面的方法,获取这个时期的高度

 

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
 
 
  1. //获取Bar和Title的高度,完成auther布局的margenTop设置  
  2.         ViewTreeObserver viewTreeObserver = fl_top.getViewTreeObserver();  
  3.         viewTreeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {  
  4.             @Override  
  5.             public boolean onPreDraw() {  
  6.   
  7.                 if (!hasMeasured) {  
  8.                     FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(FrameLayout  
  9.                             .LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT);  
  10.                     layoutParams.setMargins(0, img_bar.getHeight() + tv_title.getHeight(), 00);  
  11.                     img_author.setLayoutParams(layoutParams);  
  12.                     hasMeasured = true;  
  13.                 }  
  14.                 return true;  
  15.             }  
  16.         });  


     获取了高度之后,我们就可以正确地设置位置了。但是,如果保证上面的布局随着我们的内容的移动,而改变现实状态呢?

 

    经过我手动直观测试,知乎的这个界面是根据一个固定的值,来改变显示状态的,因此,我们可以监听ScrollView的滑动距离,来判断。但是ScrollView并没有给我们这样一个监听器,咋办?重写!

 

[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
 
 
  1. /** 
  2.  * Created by zhaokaiqiang on 15/2/26. 
  3.  */  
  4. public class MyScrollView extends ScrollView {  
  5.   
  6.     private BottomListener bottomListener;  
  7.   
  8.     private onScrollListener scrollListener;  
  9.   
  10.   
  11.     public MyScrollView(Context context) {  
  12.         this(context, null);  
  13.     }  
  14.   
  15.     public MyScrollView(Context context, AttributeSet attrs) {  
  16.         super(context, attrs);  
  17.     }  
  18.   
  19.     protected void onScrollChanged(int l, int t, int oldl, int oldt) {  
  20.         super.onScrollChanged(l, t, oldl, oldt);  
  21.         if (getScrollY() + getHeight() >= computeVerticalScrollRange()) {  
  22.   
  23.             if (null != bottomListener) {  
  24.                 bottomListener.onBottom();  
  25.             }  
  26.   
  27.         }  
  28.   
  29.         if (null != scrollListener) {  
  30.             scrollListener.onScrollChanged(l, t, oldl, oldt);  
  31.         }  
  32.   
  33.   
  34.     }  
  35.   
  36.     public void setBottomListener(BottomListener bottomListener) {  
  37.         this.bottomListener = bottomListener;  
  38.     }  
  39.   
  40.     public void setScrollListener(onScrollListener scrollListener) {  
  41.   
  42.         this.scrollListener = scrollListener;  
  43.   
  44.     }  
  45.   
  46.   
  47.     public interface onScrollListener {  
  48.   
  49.         public void onScrollChanged(int l, int t, int oldl, int oldt);  
  50.   
  51.     }  
  52.   
  53.   
  54.     public interface BottomListener {  
  55.   
  56.         public void onBottom();  
  57.   
  58.     }  
  59.   
  60.   
  61. }  


    我们只需要重写onScrollChange()方法即可,在里面不光可以时时的得到位置的变化,还添加了一个BottomListener接口来监听滑动到底部的事件,写好之后就很简单了

 

 

 

[html]  view plain copy 在CODE上查看代码片 派生到我的代码片
 
 
  1. mScroller.setBottomListener(this);  
  2. mScroller.setScrollListener(this);  
[java]  view plain copy 在CODE上查看代码片 派生到我的代码片
 
 
  1. /** 
  2.      * 显示上部的布局 
  3.      */  
  4.     private void showTop() {  
  5.   
  6.         ObjectAnimator anim1 = ObjectAnimator.ofFloat(img_bar, "y", img_bar.getY(),  
  7.                 0);  
  8.         anim1.setDuration(TIME_ANIMATION);  
  9.         anim1.start();  
  10.   
  11.         ObjectAnimator anim2 = ObjectAnimator.ofFloat(tv_title, "y", tv_title.getY(),  
  12.                 img_bar.getHeight());  
  13.         anim2.setInterpolator(new DecelerateInterpolator());  
  14.         anim2.setDuration(TIME_ANIMATION + 200);  
  15.         anim2.start();  
  16.   
  17.         ObjectAnimator anim4 = ObjectAnimator.ofFloat(fl_top, "y", fl_top.getY(),  
  18.                 0);  
  19.         anim4.setDuration(TIME_ANIMATION);  
  20.         anim4.start();  
  21.   
  22.         isTopHide = false;  
  23.     }  
  24.   
  25.   
  26.     /** 
  27.      * 隐藏上部的布局 
  28.      */  
  29.     private void hideTop() {  
  30.   
  31.         ObjectAnimator anim1 = ObjectAnimator.ofFloat(img_bar, "y"0,  
  32.                 -img_bar.getHeight());  
  33.         anim1.setDuration(TIME_ANIMATION);  
  34.         anim1.start();  
  35.   
  36.         ObjectAnimator anim2 = ObjectAnimator.ofFloat(tv_title, "y", tv_title.getY(),  
  37.                 -tv_title.getHeight());  
  38.         anim2.setDuration(TIME_ANIMATION);  
  39.         anim2.start();  
  40.   
  41.         ObjectAnimator anim4 = ObjectAnimator.ofFloat(fl_top, "y"0,  
  42.                 -(img_bar.getHeight() + tv_title.getHeight()));  
  43.         anim4.setDuration(TIME_ANIMATION);  
  44.         anim4.start();  
  45.   
  46.         isTopHide = true;  
  47.     }  
  48.   
  49.     @Override  
  50.     public void onBottom() {  
  51.         if (isToolsHide) {  
  52.             showTools();  
  53.         }  
  54.     }  
  55.   
  56.     @Override  
  57.     public void onScrollChanged(int l, int t, int oldl, int oldt) {  
  58.   
  59.         if (t <= dp2px(TOP_DISTANCE_Y) && isTopHide && isAnimationFinish) {  
  60.             showTop();  
  61.             Log.d(TAG, "显示");  
  62.         } else if (t > dp2px(TOP_DISTANCE_Y) && !isTopHide && isAnimationFinish) {  
  63.             hideTop();  
  64.             Log.d(TAG, "隐藏");  
  65.         }  
  66.     }  


    我们只需要根据当前的位置,来实现布局的显示和隐藏就可以啦!

 

 

    OK,这篇文章就到这里,如果你有疑问或者是建议,都可以评论或者是私信。

    下载地址:https://github.com/ZhaoKaiQiang/ZhiHuDetailDemo

 

----------------------------------------分割线---------------------------------------------

    咱们上面咱们是用ScrollView代替的WebView,有的同学可能问了,如果也要用WebView咋办呢?

    这里给出两种方案。

    第一种,JS和Android交互,用JS传递网页的移动位置,没试过,只是一个思路,不知道能不能实现。

    第二种,就是集成WebView,他也有onScrollChanged(),剩下的就不用多说了~

----------------------------------------分割线--------------------------------------------- 

 

昨天又体验了下,发现知乎还有个功能,就是点击屏幕,可以实现上部布局和Tools布局的隐藏和显示,这个其实也不难,我们可以自己定义一个GestureDetector,然后实现Gesturedetector.SimpleOnGestureListener,onDown返回true,然后在onSingleTapConfirmed里面调用hideXXX和showXXX即可,仅提供思路,剩下的就自己去做吧,你一定可以的!

猜你喜欢

转载自lishuaishuai.iteye.com/blog/2290803