Android 仿安居客房源详情页图片显示

2,545 阅读8分钟

效果

首先把安居客房源详情页的效果展示出来:

分析

这是一个图片展示效果,整个页面可以看出,分为两层:第一层,图片展示;第二层,一个详情页的显示。而图片展示页面刚进入的时候,
是一个被收缩的页面,我们可以通过下滑和上滑来展开和收缩图片页面。同时,图片的浏览页面由ViewPager + TabLayout构成。但是在这里,
一个TabLayout 控制一个ViewPager页面,而一个ViewPager页面中又可以滑动多个图片,到底后再切换Tab页面。因此,我们通过ViewPager嵌套
ViewPager的方式来实现这种效果。
接下来就是滑动的处理。我们通过一个自定义的父布局,添加ViewDragHelper,来对子View进行滑动处理。主要需要处理的是:1、房源详情
和图片详情的measure和layout;2、当图片页面上下滑动的时候,详情页面也会滑动,但是滑动更快,反之亦然;3、当图片详情没有展开时,下滑,
会展开图片页面;4、图片展开时,上滑,会折叠页面。

基本的分析已经完成,那接下来就具体去一步一步实现。

具体实现:

图片的显示:

图片的显示我们需要在ViewPager中嵌套ViewPager,同时添加TabLayou控件。为了达到,当前类型的图片没有滑动完,则继续滑动当前图片,否则滑动到下一个ViewPager的效果,我们需要重写外层的ViewPager:
OuterViewPager:
/**
 * 外层的ViewPager,这样可以嵌套滑动
 *
 * Created by xiaoqi on 2016/11/28.
 */

public class OuterViewPager extends ViewPager {
   public OuterViewPager(Context context, AttributeSet attrs) {
      super(context, attrs);
   }

   public OuterViewPager(Context context) {
      super(context);
   }
   protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
      if(v != this && v instanceof ViewPager) {
         int currentItem = ((ViewPager) v).getCurrentItem();
         int countItem = ((ViewPager) v).getAdapter().getCount();
         if((currentItem==(countItem-1) && dx<0) ||="" (currentitem="=0" &&="" dx="">0)){
            return false;
         }
         return true;
      }
      return super.canScroll(v, checkV, dx, x, y);
   }
}

接下来我们先把不能滑动的详情页给展示出来:
布局文件:
xml version="1.0" encoding="utf-8"?>
    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:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="xiaoqi.collapseviewdemo.MainActivity">

            android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/black"
        android:tag="layout_show">

                    android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:text="页码"
            android:textSize="17sp"
            android:visibility="visible"/>

                    android:id="@+id/outerViewPager"
            android:layout_width="match_parent"
            android:layout_height="400dp"
            android:layout_centerInParent="true"
            android:visibility="visible">
        

                    android:id="@+id/tabLayout"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_alignParentBottom="true"
            android:visibility="visible"
            app:tabSelectedTextColor="@android:color/holo_green_light"
            app:tabTextColor="@android:color/white">
        
    

            android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:tag="layout_content">

                    android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical">

                            android:id="@+id/textView"
                android:layout_width="match_parent"
                android:layout_height="400dp"
                android:background="@android:color/darker_gray"/>

                            android:layout_width="match_parent"
                android:layout_height="400dp"
                android:layout_marginTop="10dp"
                android:background="@android:color/darker_gray"/>

                            android:layout_width="match_parent"
                android:layout_height="400dp"
                android:layout_marginTop="10dp"
                android:background="@android:color/darker_gray"/>
        
    

当前这个页面是一个线性布局,由两个部分组成上面的RelativeLayou即是图片的详情页,下面的ScrollView则是房源的详情页,图片的详情页是一个OuterViewPager + TabLayout,每一个OuterViewPager中各是一个ViewPager,里面的ViewPager中则是每个Tab对应的图片集合。
这部分比较简单,直接贴出当前Activity完整代码了:
package xiaoqi.collapseviewdemo;

import android.graphics.Matrix;
import android.support.design.widget.TabLayout;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.Toast;

import java.util.ArrayList;

import static android.R.attr.width;
import static android.os.Build.VERSION_CODES.M;

public class MainActivity extends AppCompatActivity {
   OuterViewPager outerViewPager;
   TabLayout tabLayout;
   CollapseLayout vdhLayout;
   ArrayList tabList = new ArrayList<>();
   ArrayList imageList = new ArrayList<>();
   ArrayList imageViewList = new ArrayList<>();
   ArrayList outerViewPagerList = new ArrayList<>();
   int width;
   Matrix matrix;

   @Override
   protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
      width = getResources().getDisplayMetrics().widthPixels;
      matrix = new Matrix();
//    vdhLayout = (VDHLayout) findViewById(R.id.vdhLayout);
      outerViewPager = (OuterViewPager) findViewById(R.id.outerViewPager);
      tabLayout = (TabLayout) findViewById(R.id.tabLayout);
      tabList.add("户型图");
      tabList.add("户型图");
      tabList.add("户型图");
      tabList.add("户型图");
      tabList.add("户型图");
      tabList.add("户型图");
      tabList.add("户型图");
      imageList.add(R.drawable.pic4);
      imageList.add(R.drawable.pic2);
      imageList.add(R.drawable.pic5);
      tabLayout.setTabMode(TabLayout.MODE_SCROLLABLE);
      for(int i=0;i
完成好上面以后,一个不能折叠的详情页已经完成。效果如下:


接下来就是关健的处理滑动的过程:

滑动的处理

这里通过自定义一个ViewGroup,替代之前最外层的LinearLayout,我们通过重写onMeasure、onMeasure方法,并且通过ViewDragHelper来处理滑动。

首先重写onMeasure方法,因为onMeasure是ViewGroup绘制的第一步:

首先我们获得当前这个屏幕的宽高,用于子布局的measure:
int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
接下来,分别测量图片详情页和房源详情页两个布局的大小
房源详情页比较简单,即宽为屏幕宽度,高为屏幕高度:
int picShowWidthSpec = MeasureSpec.makeMeasureSpec(sizeWidth,MeasureSpec.EXACTLY);
int picShowHeightSpec = MeasureSpec.makeMeasureSpec(sizeHeight,MeasureSpec.EXACTLY);
rlShow.measure(picShowWidthSpec,picShowHeightSpec);
测量详情页,即ScrollView大小。因为ScrollView是高度是wrap_parent的因此,我们需要去重新测量一下他的高度,否则,它将不会被显示出来:
int w = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
int h = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
llContent.measure(w, h);
int height = llContent.getMeasuredHeight();
int contentWidthSpec = MeasureSpec.makeMeasureSpec(sizeWidth,MeasureSpec.EXACTLY);
int contentHeightSpec = MeasureSpec.makeMeasureSpec(height,MeasureSpec.EXACTLY);
llContent.measure(contentWidthSpec,contentHeightSpec);

接下来是Layout的过程:
这个自定义View是继承FrameLayout,因此,我们必须重写Layout达到垂直布局的效果。

rlShow是图片页面,llContent是详情页面。

通过观察可以看到,在我们进入页面的时候,图片详情页的高度是屏幕的三分之一,剩下的就是房源详情。同时,当ViewPager滑动的时候,回去重新调用onLayout,因此我们需要去判断当前详情页是否展开:
1、如果展开:rlShow的高度为屏幕高度,llContent在他下面
2、如果没有展开:llContent距离屏幕顶部的位置为屏幕高度的三分之一,rlShow顶部也在屏幕外面的三分之一的位置
代码实现:
if(hasExpand){
   rlShow.layout(0 , 0 , getMeasuredWidth() ,height);
   llContent.layout(0, height,getMeasuredWidth(), llContent.getMeasuredHeight() + height);
}else {
   rlShow.layout(0, - initPicHeight , getMeasuredWidth(), height - initPicHeight);
   llContent.layout(0, initPicHeight,getMeasuredWidth(), llContent.getMeasuredHeight() + initPicHeight);
}

接下来便是实现滑动过程:
具体滑动我们要实现的是:

1、rlShow的滑动范围不能超过当前屏幕
2、llContent的底部不能滑动到屏幕中
3、在滑动的时候,图片页面滑动的距离总是详情页面的一半
4、当llContent距离屏幕顶部三分之一处时,如果向上滑,则不处理,向下滑,松开后,就展开页面
5、当图片详情展开时,上滑rlShow,如果松开的位置超过底部TabLayout的高度,就收缩页面

下面用代码来分别实现,我们需要定义一个ViewDragHelper,重写下面几个方法:

tryCaptureView 确定需要监控滑动的view
clampViewPositionVertical 处理垂直滑动的范围
onViewPositionChanged 模拟出层叠滑动的效果
onViewRelased 当滑动释放以后,处理展开和收缩效果

具体代码:
tryCaptureView:
我们要处理滑动的只有两个view
@Override
public boolean tryCaptureView(View child, int pointerId) {
   Log.i(TAG,"tryCaptureView");
   return child == llContent || child == rlShow;
}

clampViewPositionVertical:
我们要处理滑动的范围,同时滑动的时候,TabLayout隐藏
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
   Log.i(HEIGHT_TAG,"clampViewPositionVertical:"+top);
   int newTop = top;
   if(child == llContent){
      int topBounds =  getMeasuredHeight() - llContent.getMeasuredHeight();
      int bottomBounds = getMeasuredHeight();
      if(top < topBounds){
         newTop = topBounds;
      }else if(top > bottomBounds){
         newTop = bottomBounds;
      }else {
         newTop = top;
      }
   }else if(child == rlShow){
      if(llContent.getTop() > getMeasuredHeight()+systemBarHeight){
         newTop = rlShow.getTop();
      }
   }
   tabLayout.setVisibility(GONE);
   return newTop;
}

onViewPositionChanged:
模拟层叠滑动效果,图片详情滑动的距离是房源详情滑动距离的一半:
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, final int dy) {
   if(changedView == llContent){
      rlShow.offsetTopAndBottom(dy/2);
   }else if(changedView == rlShow){
      llContent.offsetTopAndBottom(2 * dy);
   }
}

onViewRelased:
释放的时候事件处理:
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
   super.onViewReleased(releasedChild, xvel, yvel);
   //releasedChild到屏幕顶部的距离
   int offsetToTop = llContent.getTop();
   //当显示内容最上位置的坐标>初始图片高度时才会去展开和缩小
   if(offsetToTop > initPicHeight){
      if(offsetToTop >= tabLayout.getTop()){
         expendPicView();
      }else if(!hasExpand){
         expendPicView();
      }else {
         foldPicView();
      }
   }else {
      hasExpand = false;
      tabLayout.setVisibility(GONE);
   }
}
expendPicView 展开图片详情的方法:
/**
 * 展开图片浏览界面
 */
private void expendPicView() {
   hasExpand = true;
   viewDragHelper.smoothSlideViewTo(llContent,0,height);
   tabLayout.setVisibility(VISIBLE);
   postInvalidate();
}

foldPicView 收缩图片详情的方法:
/**
 * 折叠图片浏览界面
 */
private void foldPicView() {
   hasExpand = false;
   viewDragHelper.smoothSlideViewTo(llContent,0,initPicHeight);
   tabLayout.setVisibility(GONE);
   postInvalidate();
}
注意:这里用的是smoothSlideViewTo方法而不是settleCapturedViewAt方法,因为后者为了模拟手势滑动有一个垂直滑动速度导致位置便宜的问题,而前置调用的时候,垂直速度置成了0。

大致的方法说完了,所有的代码如下:
package xiaoqi.collapseviewdemo;

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.RelativeLayout;

/**
 * Created by xiaoqi on 2016/11/25.
 */

public class CollapseLayout extends FrameLayout {
   View rlShow;
   View llContent;
   View tabLayout;
   View viewPager;

   private final static String TAG = "VDHLayout";
   private final static String HEIGHT_TAG = "DEMANDS";
   ViewDragHelper viewDragHelper;
   int width;
   int height;
   //系统状态栏高度
   int systemBarHeight;

   boolean hasExpand = false;
   //初始的图片高度
   int initPicHeight ;
   Context context;

   public CollapseLayout(Context context, AttributeSet attrs) {
      super(context, attrs);
      this.context = context;
      width = getResources().getDisplayMetrics().widthPixels;
      height = getResources().getDisplayMetrics().heightPixels;
      Resources resources = context.getResources();
      int resourceId = resources.getIdentifier("status_bar_height", "dimen","android");
      systemBarHeight = resources.getDimensionPixelSize(resourceId);
      initPicHeight = height / 3 - dip2px(context, 50);
      viewDragHelper = ViewDragHelper.create(this,1.0f, new ViewDragHelper.Callback() {
         @Override
         public boolean tryCaptureView(View child, int pointerId) {
            Log.i(TAG,"tryCaptureView");
            return child == llContent || child == rlShow;
         }

         @Override
         public int clampViewPositionVertical(View child, int top, int dy) {
            Log.i(HEIGHT_TAG,"clampViewPositionVertical:"+top);
            int newTop = top;
            if(child == llContent){
               int topBounds =  getMeasuredHeight() - llContent.getMeasuredHeight();
               int bottomBounds = getMeasuredHeight();
               if(top < topBounds){
                  newTop = topBounds;
               }else if(top > bottomBounds){
                  newTop = bottomBounds;
               }else {
                  newTop = top;
               }
            }else if(child == rlShow){
               if(llContent.getTop() > getMeasuredHeight()+systemBarHeight){
                  newTop = rlShow.getTop();
               }
            }
            tabLayout.setVisibility(GONE);
            return newTop;
         }

         @Override
         public void onViewPositionChanged(View changedView, int left, int top, int dx, final int dy) {
            if(changedView == llContent){
               rlShow.offsetTopAndBottom(dy/2);
            }else if(changedView == rlShow){
               llContent.offsetTopAndBottom(2 * dy);
            }
         }

         @Override
         public void onViewCaptured(View capturedChild, int activePointerId) {
            super.onViewCaptured(capturedChild, activePointerId);
         }

         @Override
         public void onViewReleased(View releasedChild, float xvel, float yvel) {
            super.onViewReleased(releasedChild, xvel, yvel);
            //releasedChild到屏幕顶部的距离
            int offsetToTop = llContent.getTop();
            //当显示内容最上位置的坐标>初始图片高度时才会去展开和缩小
            if(offsetToTop > initPicHeight){
               if(offsetToTop >= tabLayout.getTop()){
                  expendPicView();
               }else if(!hasExpand){
                  expendPicView();
               }else {
                  foldPicView();
               }
            }else {
               hasExpand = false;
               tabLayout.setVisibility(GONE);
            }
         }

         @Override
         public int getViewVerticalDragRange(View child) {
            return  getMeasuredHeight();
         }


      });
   }

   @Override
   public boolean onTouchEvent(MotionEvent event) {
      viewDragHelper.processTouchEvent(event);
      return true;
   }

   @Override
   public boolean onInterceptTouchEvent(MotionEvent ev) {
      return viewDragHelper.shouldInterceptTouchEvent(ev);
   }

   @Override
   public void computeScroll()
   {
      if(viewDragHelper.continueSettling(true))
      {
         ViewCompat.postInvalidateOnAnimation(this);
         postInvalidate();
//       invalidate();
      }
   }

   @Override
   protected void onFinishInflate() {
      super.onFinishInflate();
      Log.i(TAG,"onFinishInflate");
      rlShow = getChildAt(0);
      llContent = getChildAt(1);
      tabLayout = ((RelativeLayout)rlShow).getChildAt(2);
      viewPager = ((RelativeLayout)rlShow).getChildAt(1);
      RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) viewPager.getLayoutParams();
      params.height = height/2;
      viewPager.setLayoutParams(params);
   }

   @Override
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
      Log.i(TAG,"onMeasure");
      super.onMeasure(widthMeasureSpec, heightMeasureSpec);
      int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
      int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
//    setMeasuredDimension(sizeWidth, sizeHeight);

      int w = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
      int h = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
      llContent.measure(w, h);
      int height = llContent.getMeasuredHeight();
      int contentWidthSpec = MeasureSpec.makeMeasureSpec(sizeWidth,MeasureSpec.EXACTLY);
      int contentHeightSpec = MeasureSpec.makeMeasureSpec(height,MeasureSpec.EXACTLY);
      llContent.measure(contentWidthSpec,contentHeightSpec);

      int picShowWidthSpec = MeasureSpec.makeMeasureSpec(sizeWidth,MeasureSpec.EXACTLY);
      int picShowHeightSpec = MeasureSpec.makeMeasureSpec(sizeHeight,MeasureSpec.EXACTLY);
      rlShow.measure(picShowWidthSpec,picShowHeightSpec);

   }

   @Override
   protected void onLayout(boolean changed, int l, int t, int r, int b) {
      super.onLayout(changed, l, t, r, b);
      Log.i(TAG,"hasExpand:"+hasExpand);
      if(hasExpand){
         rlShow.layout(0 , 0 , getMeasuredWidth() ,height);
         llContent.layout(0, height,getMeasuredWidth(), llContent.getMeasuredHeight() + height);
      }else {
         rlShow.layout(0, - initPicHeight , getMeasuredWidth(), height - initPicHeight);
         llContent.layout(0, initPicHeight,getMeasuredWidth(), llContent.getMeasuredHeight() + initPicHeight);
      }
   }

   /**
    * 折叠图片浏览界面
    */
   private void foldPicView() {
      hasExpand = false;
      viewDragHelper.smoothSlideViewTo(llContent,0,initPicHeight);
      tabLayout.setVisibility(GONE);
      postInvalidate();
   }

   /**
    * 展开图片浏览界面
    */
   private void expendPicView() {
      hasExpand = true;
      viewDragHelper.smoothSlideViewTo(llContent,0,height);
//    if(releasedChild == llContent){
//       viewDragHelper.settleCapturedViewAt(0,height);
//    }else if(releasedChild == rlShow){
//       viewDragHelper.settleCapturedViewAt(0, 0);
//    }
      tabLayout.setVisibility(VISIBLE);
      postInvalidate();
   }

   public static int dip2px(Context context, float dipValue){
      final float scale = context.getResources().getDisplayMetrics().density;
      return (int)(dipValue * scale + 0.5f);
   }

   public void viewPagerClick(){
      if(hasExpand){
         foldPicView();
      }else{
         expendPicView();
      }
   }
}