Android ViewPager实现3D Gallery效果.

使用ViewPager打造的3D画廊,先看效果图:

GIF.gif

需求点:

        1.中间item放大

        2.中间item覆盖在两侧item上

        3.点击或者滑动两侧的item可以切换ViewPager的当前展示页面

所以首先我们想到的肯定是ViewGroup的clipChildren属性,设为false,可以让子view突破ViewGroup的限制呈现出来突出来的效果,或者是本文这种ViewPager的覆盖效果,网上也看到用RecyclerView实现类似效果的,有兴趣的自行百度.

下面我们看代码怎么实现:

首先在我的项目中这个3D Gallery是一个RecyclerView的Item,并且外部的界面是可左右滑动切花的Fragment,先说明一下下面有些逻辑会处理滑动冲突.
先看下布局,主界面只有一个RecyclerView就不贴代码了.

看下Gallery的布局吧:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:id="@+id/rl_vp_container"
android:orientation="vertical">
<com.bubu.a3dgallerydemo.StarView
    android:layout_toLeftOf="@+id/viewpager"
    android:id="@+id/view_left"
    android:layout_width="match_parent"
    android:layout_height="303dp"
    />
<com.bubu.a3dgallerydemo.StarView
    android:layout_toRightOf="@+id/viewpager"
    android:id="@+id/view_right"
    android:layout_width="match_parent"
    android:layout_height="303dp"
    />
<com.bubu.a3dgallerydemo.CustomViewPager
    android:layout_centerHorizontal="true"
    android:layout_width="200dp"
    android:layout_height="303dp"
    android:id="@+id/viewpager"
/>
<TextView
    android:layout_centerHorizontal="true"
    android:layout_below="@+id/viewpager"
    android:layout_marginTop="8dp"
    android:id="@+id/tv_star_desc"
    android:layout_gravity="center_horizontal"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
/>
</RelativeLayout>

这里可以看出虽然我们的ViewPager看起来是铺满屏幕的,但是其实除了中间的Item两侧是用StarView进行占位的,它的作用是拦截处理点击和触摸两侧可以切换ViewPager的展示Item,但是由于项目中整个界面是可以左右滑动切换的,所以我稍后的代码中拦截了横向滑动,如果不需要请自行忽略.还有个ViewPager的Item的布局也贴一下吧:

    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent"
                  android:orientation="vertical"
                  android:gravity="center_horizontal"
                  android:scaleX="0.85"
                  android:scaleY="0.85"
                  android:elevation="6dp"
    >
        <RelativeLayout
    
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical" >
    
                <ImageView
                    android:adjustViewBounds="true"
                    android:id="@+id/iv_star_item"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:scaleType="fitXY"
                    />
    
                <LinearLayout
                    android:layout_marginRight="6dp"
                    android:layout_marginBottom="8dp"
                    android:layout_alignParentRight="true"
                    android:layout_alignParentBottom="true"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:orientation="horizontal"
                    android:background="#00000000"
                >
                <TextView
                    android:gravity="bottom"
                    android:id="@+id/tv_index"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:textColor="#FFFFFFFF"
                    android:textSize="20sp"
                    android:shadowDy="2"
                    android:shadowDx="0"
                    android:shadowRadius="4"
                    android:text="2"
                    android:shadowColor="#80000000"/>
                <TextView
                    android:gravity="bottom"
                    android:id="@+id/tv_star_total"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:textColor="#FFFFFFFF"
                    android:textSize="12sp"
                    android:shadowDy="2"
                    android:shadowDx="0"
                    android:shadowRadius="4"
                    android:text="/10"
                    android:shadowColor="#80000000" />
                </LinearLayout>
            <ImageView
                android:visibility="visible"
                android:id="@+id/iv_cover"
                android:src="@mipmap/bg_40black"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:scaleType="centerCrop"
            />
        </RelativeLayout>
    </LinearLayout>

简单说下,左后一个ImageView是做两侧小的Item半透明效果的,直接覆盖了一层半透明效果的图片在上面.在ViewPager切换效果的管理类中管理缩放和透明度,看下代码:
package com.bubu.a3dgallerydemo;

import android.content.Context;
import android.support.v4.view.ViewPager;
import android.view.View;
import android.widget.ImageView;

/**
 * Fixme
 * Author: LWJ
 * desc:
 * Date: 2017/09/19 10:13
 * Copyright (c) 2016 d2cmall. All rights reserved.
 */

public class RotationPageTransformer implements ViewPager.PageTransformer{
    private static final float MIN_SCALE=0.85f;
    private static final float MIN_ALPHA=0.6f;

    public void setContext(Context context) {
        mContext = context;
    }

    private Context mContext;
    @Override
    public void transformPage(View page, float position) {
        float scaleFactor = Math.max(MIN_SCALE,1 - Math.abs(position));
        float scaleAlpha = Math.max(MIN_ALPHA,1 - Math.abs(position));
        ImageView imageTag= (ImageView) page.findViewById(R.id.iv_cover);
        float rotate = 0;
        //position小于等于1的时候,代表page已经位于中心item的最左边,
        //此时设置为最小的缩放率以及最大的旋转度数
        if (position <= -1){
            page.setScaleX(MIN_SCALE);
            page.setScaleY(MIN_SCALE);
            imageTag.setAlpha((float) 1);
        }//position从0变化到-1,page逐渐向左滑动
        else if (position < 0){
            imageTag.setAlpha(Math.abs(position));
            page.setScaleX(scaleFactor);
            page.setScaleY(scaleFactor);
        }//position从0变化到1,page逐渐向右滑动
        else if (position >=0 && position < 1){
            imageTag.setAlpha(position);
            page.setScaleX(scaleFactor);
            page.setScaleY(scaleFactor);
        }//position大于等于1的时候,代表page已经位于中心item的最右边
        else if (position >= 1){
            imageTag.setAlpha((float)1);
            page.setScaleX(scaleFactor);
            page.setScaleY(scaleFactor);
        }


    }
}

然后看下给ViewPager数据以及拦截左右滑动,贴下代码:
package com.bubu.a3dgallerydemo;

import android.content.Context;
import android.content.Intent;
import android.support.v4.view.ViewPager;
import android.support.v7.widget.RecyclerView;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

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

/**
 * Created by Administrator on 2018/5/10.
 * Description : MainAdapter
 */

public class MainAdapter extends RecyclerView.Adapter {
    private Context mContext;
    private float downX ;    //按下时 的X坐标
    private float downY ;    //按下时 的Y坐标
    private int currentPosition;
    private int mOriginSize;
    private List<String> dataList=new ArrayList<>();

    public MainAdapter(Context context,List<String> list) {
        this.mContext=context;
        dataList.addAll(list);
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(mContext).inflate(R.layout.layout_main_star1, parent, false);
        return new MainStarViewHolder(view);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        //  所以在空白的左右两侧各填充了透明的View(StarView自定义View拦截左右方向滑动事件),用于交互,左侧View向右滑动和点击ViewPager切换
        // ,右侧View向左滑动和点击切换ViewPager,其它不做处理
        final MainStarViewHolder mainStarHolder = (MainStarViewHolder) holder;
        mainStarHolder.mViewPager.setPageTransformer(true, new RotationPageTransformer());
        mainStarHolder.mViewPager.setOffscreenPageLimit(2);
        DisplayMetrics dm = mContext.getApplicationContext().getResources().getDisplayMetrics();
        //不同分辨率适配
        int width = dm.widthPixels;
        if (width > 800 && width <= 1080) {
            mainStarHolder.mViewPager.setPageMargin(-200);
        } else if (width > 1080) {
            mainStarHolder.mViewPager.setPageMargin(-280);
        } else {
            mainStarHolder.mViewPager.setPageMargin(-180);
        }

        mainStarHolder.viewLeft.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) { //左侧透明View仅在向右滑时切换ViewPager
                float x= event.getX();
                float y = event.getY();
                switch (event.getAction()){
                    case MotionEvent.ACTION_DOWN:
                        //将按下时的坐标存储
                        downX = x;
                        downY = y;
                        break;
                    case MotionEvent.ACTION_UP:
                    case MotionEvent.ACTION_CANCEL:
                        //获取到距离差
                        float dx= x-downX;
                        float dy = y-downY;
                        //防止是按下也判断
                        //通过距离差判断方向
                        int orientation = getOrientation(dx, dy);
                        switch (orientation) {
                            case 'r':       //向右滑动
                                mainStarHolder.mViewPager.setCurrentItem(mainStarHolder.mViewPager.getCurrentItem()-1,true);
                                return true;
                            case '0':   //点击
                                mainStarHolder.mViewPager.setCurrentItem(mainStarHolder.mViewPager.getCurrentItem()-1,true);
                                return true;
                            case 'l':       //向左滑动
                                return true;
                        }
                        break;
                }
                return true;
            }
        });
        mainStarHolder.viewRight.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {     //右侧透明View仅在向右滑时切换ViewPager
                float x= event.getX();
                float y = event.getY();
                switch (event.getAction()){
                    case MotionEvent.ACTION_DOWN:
                        //将按下时的坐标存储
                        downX = x;
                        downY = y;
                        break;
                    case MotionEvent.ACTION_UP:
                    case MotionEvent.ACTION_CANCEL:
                        //获取到距离差
                        float dx= x-downX;
                        float dy = y-downY;
                        //通过距离差判断方向
                        int orientation = getOrientation(dx, dy);
                        switch (orientation) {
                            case 'r':       //向右滑动
                                return true;
                            case '0':       //点击
                                mainStarHolder.mViewPager.setCurrentItem(mainStarHolder.mViewPager.getCurrentItem()+1,true);
                                return true;
                            case 'l':       //向左滑动
                                if(event.getAction()== MotionEvent.ACTION_UP || event.getAction()== MotionEvent.ACTION_CANCEL ){
                                    mainStarHolder.mViewPager.setCurrentItem(mainStarHolder.mViewPager.getCurrentItem()+1,true);
                                }
                                return true;
                        }
                        break;
                }
                return true;
            }
        });
        mOriginSize = dataList.size();
        initStarAdapter(mainStarHolder, dataList);
        mainStarHolder.mViewPager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

            }

            @Override
            public void onPageSelected(int position) {
                currentPosition = position;
                mainStarHolder.mViewPager.setTranslationX(1);
            }

            @Override
            public void onPageScrollStateChanged(int state) {
            }
        });
    }

    @Override
    public int getItemCount() {
        return 1;
    }

    private void initStarAdapter(MainStarViewHolder mainStarHolder, List<String> dataList) {
        final MainStarAdapter mainStarAdapter = new MainStarAdapter(dataList, mContext, mainStarHolder.mViewPager, mOriginSize);
        mainStarHolder.mViewPager.setAdapter(mainStarAdapter);
        mainStarHolder.mViewPager.setCurrentItem( 20*mOriginSize + currentPosition);
    }
        //获取滑动方向
    private int getOrientation(float dx, float dy) {
        if (Math.abs(dx)>Math.abs(dy)){
            //X轴移动
            if(dx==0){//点击
                return '0';
            }
            return dx>0?'r':'l';
        }else{
            //Y轴移动
            if(dy==0){//点击
                return '0';
            }
            return dy>0?'b':'t';
        }
    }
}

最核心的逻辑:如果用Android原生的ViewPager来实现你会发现左边的Item会覆盖在中间的Item之上.所以我们要自定义一下ViewPager,重写一下他的排序方法:

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

import java.util.ArrayList;
import java.util.Collections;

/**
 * Created by LWJ.
 */

public class CustomViewPager extends ViewPager {
    private ArrayList<Integer> childCenterXAbs = new ArrayList<>();
    private SparseArray<Integer> childIndex = new SparseArray<>();

    public CustomViewPager(Context context) {
        super(context);
        init();
    }

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

    private void init(){
        setClipToPadding(false);
        setOverScrollMode(OVER_SCROLL_NEVER);
    }


    /**
     * @param childCount
     * @param n
     * @return 第n个位置的child 的绘制索引
     */
    @Override
    protected int getChildDrawingOrder(int childCount, int n) {
        if (n == 0 || childIndex.size() != childCount) {
            childCenterXAbs.clear();
            childIndex.clear();
            int viewCenterX = getViewCenterX(this);
            for (int i = 0; i < childCount; ++i) {
                int indexAbs = Math.abs(viewCenterX - getViewCenterX(getChildAt(i)));
                //两个距离相同,后来的那个做自增,从而保持abs不同
                if (childIndex.get(indexAbs) != null) {
                    ++indexAbs;
                }
                childCenterXAbs.add(indexAbs);
                childIndex.append(indexAbs, i);
            }
            Collections.sort(childCenterXAbs);//1,0,2  0,1,2
        }
        //那个item距离中心点远一些,就先draw它。(最近的就是中间放大的item,最后draw)
        return childIndex.get(childCenterXAbs.get(childCount - 1 - n));
    }

    private int getViewCenterX(View view) {
        int[] array = new int[2];
        view.getLocationOnScreen(array);
        return array[0] + view.getWidth() / 2;
    }
}

后续会整理一些项目中比较实用的代码,不喜勿喷.
完整代码请移步github

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

推荐阅读更多精彩内容