Android-通过自定义ViewPager来高仿土巴兔选择装修风格效果

最近看到有同事在用土巴兔这个app,看了里面的一些效果非常的不错,就试着模仿了一下,首先模仿的是土巴兔里面一个选择装修风格的效果,先看原版效果图如下:
image

这种效果实现的方式很多,比如HorizonalScrollView、Recyclerview(水平)、ViewPager等等,这里我使用ViewPager来高仿,为啥通过ViewPager来高仿呢,一是ViewPager提供了PageTransformer,很容易的实现切换动画效果,二是ViewPager很容易控制滑动选中的一项居中,这里先看下高仿后的效果图,效果还是很不错的:
image


要实现这种效果,核心知识点有如下几个:

  1. android:clipChildren设置为false,意味着不限制子View在其范围内,也就是说子view可以超出父view的范围
  2. 通过PageTransformer来实现缩放动画
  3. 拦截点击事件的位置来实现点击切换viewpager,这个为什么要实现,后面再说

下面我们一步一步讲解来实现这个布局效果

定义布局xml

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout 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/page_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white"
    android:clipChildren="false"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context=".MainActivity"
    tools:showIn="@layout/activity_main">

    <com.hhl.tubatu.ClipViewPager
        android:id="@+id/viewpager"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_centerInParent="true"
        android:clipChildren="false"
        android:overScrollMode="never" />

</RelativeLayout>

上面的RelativeLayout和自定义的ClipViewPager都各自添加了一个属性android:clipChildren=”false”,clipChildren的意思是是否限制子View在其范围内,这个默认是true,也就是默认是限制子view在其范围的

给ViewPager设置缩放动画,这里通过PageTransformer来实现

mViewPager = (ClipViewPager) findViewById(R.id.viewpager);
    mViewPager.setPageTransformer(true, new ScalePageTransformer());

再来看ScalePageTransformer的实现,核心就是实现transformPage(View page, float position)这个方法

/**
 * Created by HanHailong on 15/9/27.
 */
public class ScalePageTransformer implements ViewPager.PageTransformer {

    public static final float MAX_SCALE = 1.2f;
    public static final float MIN_SCALE = 0.6f;

    @Override
    public void transformPage(View page, float position) {

        if (position < -1) {
            position = -1;
        } else if (position > 1) {
            position = 1;
        }

        float tempScale = position < 0 ? 1 + position : 1 - position;

        float slope = (MAX_SCALE - MIN_SCALE) / 1;
        //一个公式
        float scaleValue = MIN_SCALE + tempScale * slope;
        page.setScaleX(scaleValue);
        page.setScaleY(scaleValue);
    }
}

其实核心代码就是这个动画实现部分,这里设置了一个最大缩放和最小缩放比例,当处于最中间的view忘左边滑动时,它的position值是小于0的,并且是越来越小,它右边的view的position是从1逐渐减小到0的。

下面我们看下在Activity的完整实现代码

import android.content.Context;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;

import com.hhl.tubatu.adapter.RecyclingPagerAdapter;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {

    private ClipViewPager mViewPager;
    private TubatuAdapter mPagerAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();
            }
        });

        mViewPager = (ClipViewPager) findViewById(R.id.viewpager);
        mViewPager.setPageTransformer(true, new ScalePageTransformer());

        findViewById(R.id.page_container).setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                return mViewPager.dispatchTouchEvent(event);
            }
        });

        mPagerAdapter = new TubatuAdapter(this);
        mViewPager.setAdapter(mPagerAdapter);

        initData();
    }

    private void initData() {
        List<Integer> list = new ArrayList<>();
        list.add(R.drawable.style_xiandai);
        list.add(R.drawable.style_jianyue);
        list.add(R.drawable.style_oushi);
        list.add(R.drawable.style_zhongshi);
        list.add(R.drawable.style_meishi);
        list.add(R.drawable.style_dzh);
        list.add(R.drawable.style_dny);
        list.add(R.drawable.style_rishi);

        //设置OffscreenPageLimit
        mViewPager.setOffscreenPageLimit(list.size());
        mPagerAdapter.addAll(list);
    }

    public static class TubatuAdapter extends RecyclingPagerAdapter {

        private final List<Integer> mList;
        private final Context mContext;

        public TubatuAdapter(Context context) {
            mList = new ArrayList<>();
            mContext = context;
        }

        public void addAll(List<Integer> list) {
            mList.addAll(list);
            notifyDataSetChanged();
        }

        @Override
        public View getView(int position, View convertView, ViewGroup container) {
            ImageView imageView = null;
            if (convertView == null) {
                imageView = new ImageView(mContext);
            } else {
                imageView = (ImageView) convertView;
            }
            imageView.setTag(position);
            imageView.setImageResource(mList.get(position));
            return imageView;
        }

        @Override
        public int getCount() {
            return mList.size();
        }
    }
}

这里需要注意几点,一是 mViewPager.setOffscreenPageLimit(list.size());
这里需要将setOffscreenPageLimit的值设置成数据源的总个数,如果不加这句话,会导致左右切换异常;二是需要将整个页面的事件分发给ViewPager,不然的话只有ViewPager中间的view能滑动,其他的都不能滑动,这是肯定的,因为ViewPager总体布局就是中间那一块大小,其他的子布局都跑到ViewPager外面来了。三是你发现ViewPager加了setOnTouchListener方法后,滑动是可以了,但是点击左右两边不能切换,这里需要重写ViewPager的dispatchTouchEvent方法,下面看ClipViewPager代码:

package com.hhl.tubatu;

import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;

/**
 * Created by HanHailong on 15/9/27.
 */
public class ClipViewPager extends ViewPager {

    public ClipViewPager(Context context) {
        super(context);
    }

    public ClipViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        if (ev.getAction() == MotionEvent.ACTION_UP) {
            View view = viewOfClickOnScreen(ev);
            if (view != null) {
                setCurrentItem(indexOfChild(view));
            }
        }

        return super.dispatchTouchEvent(ev);
    }

    /**
     * @param ev
     * @return
     */
    private View viewOfClickOnScreen(MotionEvent ev) {
        int childCount = getChildCount();
        int[] location = new int[2];
        for (int i = 0; i < childCount; i++) {
            View v = getChildAt(i);
            v.getLocationOnScreen(location);
            int minX = location[0];
            int minY = getTop();

            int maxX = location[0] + v.getWidth();
            int maxY = getBottom();

            float x = ev.getX();
            float y = ev.getY();

            if ((x > minX && x < maxX) && (y > minY && y < maxY)) {
                return v;
            }
        }
        return null;
    }
}

实现原理就是手指点击屏幕,如果点击的位置恰好落在ViewPager某个子View范围内,就让ViewPager切换到哪个子View!viewOfClickOnScreen方法是获取手指点击ViewPager中的哪个子View,最后调用setCurrentItem切换到相应的子View,经过以上设置就大功告成了!

补充一下,RecyclingPagerAdapterJake WhartonAndroid大神封装的可用于复用的PagerAdapter。

最后附上源码github