ViewPager系列之-仿掌上英雄联盟皮肤浏览效果

能有一个双休的周末,对于程序员来说,也算是一件幸福的事情吧。苦逼的加了一周的班,终于可以休息放松放松了。作为一个LOL爱好者,周末最开心的事当然就是约上几个小伙伴一起开黑了。一起超神、一起连跪,也算是周末的一大乐事。这几天英雄联盟搞活动,抽到一个安妮限定皮肤,可把我乐坏了,于是马上就登陆掌盟客户端查看皮肤。进入皮肤浏览界面之后,觉得这个皮肤浏览的效果还真不错,如下图:

掌盟皮肤 浏览效果.gif

作为一个程序员,当然第一时间就是思考它是怎么实现的?我能用什么方法来实现类似的效果?于是花了半天的时间,做了一个类似的效果。因此本篇文章就分享一下如何实现这一效果。最后实现的效果如下:

仿掌盟皮肤 浏览效果.gif

思路与分析

在开始写代码之前,我们还是来分析一下界面元素,和该用什么技术来实现各个部分。

1,首先是整个界面的滑动,我们肯定一眼就能看出来,用ViewPager 实现。
2,ViewPager 滑动时有放大缩小的动画,用ViewPager.Transfoemer 轻松搞定。
3,ViewPager 显示多页(展示前后页面的部分)。
4,界面图片的形状,旋转90度的等腰梯形。这个只能用自定义View来实现了。
5,整个界面的背景为当前显示图片的高斯模糊图。

代码实现

上面分析了界面的构成元素,那么现在我们就来看一下具体的实现。

1, ViewPager 展示多页
这个问题在我们前一篇文章已经讲过,这里不再重复,就是用ViewGroup 的 clipChildren 属性,值为false。也就是在整个布局的跟节点添加下面一行代码:

android:clipChildren="false"复制代码

然后,ViewPager需要设置左右Margin,也就是前后页显示的位置

<android.support.v4.view.ViewPager
           android:id="@+id/my_viewpager"
           android:layout_width="wrap_content"
           android:layout_height="300dp"
           android:clipChildren="false"
           android:layout_marginLeft="50dp"
           android:layout_marginRight="50dp"
           android:layout_centerInParent="true"
           />复制代码

从上面的效果图可以看到,当前页和前后页的部分是有间距的,我们只需要在Item布局中左右添加margin属性:

 android:layout_marginLeft="30dp"
 android:layout_marginRight="30dp"复制代码

好了,这样ViewPager就能显示多页,并且当前页和前后页之间还有一定的间距。

2, ViewPager 切换时的动画
ViewPager 切换时的自定义动画用ViewPager.PageTransformer, 这个在上一篇文章也讲过,没看过的倒回去看一下。这里不细讲了,直接上代码:

public class CustomViewPagerTransformer implements ViewPager.PageTransformer {
    private int maxTranslateOffsetX;
    private ViewPager viewPager;
    private static final float MIN_SCALE = 0.75f;


    public CustomViewPagerTransformer(Context context) {
        this.maxTranslateOffsetX = dp2px(context, 160);
    }

    public void transformPage(View view, float position) {
        // position的可能性的值有,其实从官方示例的注释就能看出:
        //[-Infinity,-1)  已经看不到了
        // (1,+Infinity] 已经看不到了
        // [-1,1]
        // 而我们从写PageTransformer,操作View动画的重点区间就在[-1,1]
        if (viewPager == null) {
            viewPager = (ViewPager) view.getParent();
        }
        int leftInScreen = view.getLeft() - viewPager.getScrollX();
        int centerXInViewPager = leftInScreen + view.getMeasuredWidth() / 2;
        int offsetX = centerXInViewPager - viewPager.getMeasuredWidth() / 2;
        float offsetRate = (float) offsetX * 0.38f / viewPager.getMeasuredWidth();
        float scaleFactor = 1 - Math.abs(offsetRate);
        if (scaleFactor > 0) {
            view.setScaleX(scaleFactor);
            view.setScaleY(scaleFactor);
            view.setTranslationX(-maxTranslateOffsetX * offsetRate);
        }
    }

    /**
     * dp和像素转换
     */
    private int dp2px(Context context, float dipValue) {
        float m = context.getResources().getDisplayMetrics().density;
        return (int) (dipValue * m + 0.5f);
    }
}复制代码

3, 自定义多边形ImageView
多边形ImageView,我们通过自定义的方式实现,继承ImageView, 然后重写onDraw()方法。这里实现这种不规则的多边形View有两种方法。第一:使用PorterDuffXfermode,这种方法需要你给一个蒙板图片,在onDraw 方法中,先绘制蒙板图片,然后设置Paint的setXfermodePorterDuff.Mode.SRC_IN,再绘制要显示的图片,这样就能把显示的图片裁剪成蒙板的形状。第二: 使用canvas的clipPath() 方法,我们用Path 来绘制多边形,然后clipPath() 将画布裁剪成绘制的形状,然后在绘制要显示的图片。

关于PorterDuffXfermode 的更多用法,有兴趣的可以去google 一下,网上有很多相关的文章。这里我用的是两种方法的结合,先用clipPath得到一个需要形状的bitmap,然后使用PorterDuffXfermode。自定义View代码如下:

public class PolygonView extends AppCompatImageView {
    private int mWidth = 0;
    private int mHeight = 0;

    private Paint mPaint;
    private Paint mBorderPaint;

    private PorterDuffXfermode mXfermode;
    private Bitmap mBitmap;
    private int mBorderWidth;
    private Bitmap mMaskBitmap;
    public PolygonView(Context context) {
        super(context);
        init();
    }

    public PolygonView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public PolygonView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init(){
        mBorderWidth = DisplayUtils.dpToPx(4);
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);// 关闭硬件加速加速
        mPaint = new Paint();
        mPaint.setAntiAlias(true);
        mPaint.setColor(Color.RED);
        mPaint.setDither(true);

        mBorderPaint = new Paint();
        mBorderPaint.setColor(Color.WHITE);
        mBorderPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mBorderPaint.setAntiAlias(true);//抗锯齿
        mBorderPaint.setDither(true);//防抖动

        mXfermode = new PorterDuffXfermode(PorterDuff.Mode.SRC_IN);

    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mWidth = getMeasuredWidth();
        mHeight = getMeasuredHeight();

        mMaskBitmap = getMaskBitmap();

    }

    @Override
    public void setImageResource(@DrawableRes int resId) {
        super.setImageResource(resId);
        mBitmap = BitmapFactory.decodeResource(getResources(),resId);
        invalidate();
    }

    @Override
    protected void onDraw(Canvas canvas) {

        canvas.save();

        canvas.drawBitmap(mMaskBitmap,0,0,mBorderPaint);
        mPaint.setXfermode(mXfermode);

        Bitmap bitmap = getCenterCropBitmap(mBitmap,mWidth,mHeight);
        canvas.drawBitmap(bitmap,0,0,mPaint);
        mPaint.setXfermode(null);
        canvas.restore();


    }

    private Bitmap getMaskBitmap(){
        Bitmap bm = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(bm);

        Point point1 = new Point(0,30);
        Point point2 = new Point(mWidth,0);
        Point point3 = new Point(mWidth,mHeight);
        Point point4 = new Point(0,mHeight - 30);

        Path path = new Path();
        path.moveTo(point1.x,point1.y);
        path.lineTo(point2.x,point2.y);
        path.lineTo(point3.x,point3.y);
        path.lineTo(point4.x,point4.y);
        path.close();

        c.drawPath(path,mBorderPaint);

        return bm;
    }

    /**
     * 对原图进行等比裁剪
     */
    private Bitmap scaleImage(Bitmap bitmap){

        if(bitmap!=null){

            int widht=bitmap.getWidth();
            int height=bitmap.getHeight();

            int new_width=0;
            int new_height=0;

            if(widht!=height){
                if(widht>height){
                    new_height=mHeight;
                    new_width=widht*new_height/height;
                }else{
                    new_width=mWidth;
                    new_height=height*new_width/widht;
                }
            }else{
                new_width=mWidth;
                new_height=mHeight;
            }
            return Bitmap.createScaledBitmap(bitmap, new_width, new_height, true);
        }
        return null;
    }

    private Bitmap getCenterCropBitmap(Bitmap src, float rectWidth, float rectHeight) {

        float srcRatio = ((float) src.getWidth()) / src.getHeight();
        float rectRadio = rectWidth / rectHeight;
        if (srcRatio < rectRadio) {
            return Bitmap.createScaledBitmap(src, (int)rectWidth, (int)((rectWidth / src.getWidth()) * src.getHeight()), false);
        } else {
            return Bitmap.createScaledBitmap(src, (int)((rectHeight / src.getHeight()) * src.getWidth()), (int)rectHeight, false);
        }
    }

}复制代码

建议:这里使用clipPath方法的时候,会出现很多锯齿,即使Paint 设置了抗锯齿也没啥用,所以建议使用PorterDuffXfermode 方法。要实现类似的效果,最好是找设计师要一张蒙板形状图。在用PorterDuffXfermode实现,简单效果好。

通过上面的3步,其实整个 界面的效果差不多已经出来了,最后我们需要做的就是高斯模糊背景图。

4, 背景图高斯模糊
背景的高斯模糊就很简单了,前面我也有写过关于几种高斯模糊方法的对比(Android 图片高斯模糊解决方案),最后封装了一个方便的库(github.com/pinguo-zhou…),只需要简单几行代码就行。我们在ViewPager的onPageSelect方法中,获取显示的图片,进行高斯模糊处理。

      @Override
        public void onPageSelected(int position) {
            Bitmap source = BitmapFactory.decodeResource(getResources(),VPAdapter.RES[position]);
            Bitmap bitmap = EasyBlur.with(getApplicationContext())
                    .bitmap(source)
                    .radius(20)
                    .blur();
            mImageBg.setImageBitmap(bitmap);

            mDesc.setText(mVPAdapter.getPageTitle(position));
        }复制代码

最后,给出完整的布局文件和Activity代码:

1, activity布局文件:

<?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"
                android:orientation="vertical"
                android:clipChildren="false"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
       <!-- 高斯模糊背景-->
       <ImageView
           android:id="@+id/activity_bg"
           android:layout_width="match_parent"
           android:layout_height="match_parent"
           android:scaleType="centerCrop"
          />

       <!-- Toolbar-->
       <RelativeLayout
           android:id="@+id/toolbar"
           android:layout_width="match_parent"
           android:layout_height="50dp">
              <ImageView
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"
                  android:src="@drawable/navigation_back_white"
                  android:layout_centerVertical="true"
                  android:layout_marginLeft="15dp"
                  />
              <TextView
                  android:id="@+id/title_name"
                  android:layout_width="wrap_content"
                  android:layout_height="wrap_content"
                  android:layout_centerInParent="true"
                  android:textSize="18sp"
                  android:textColor="@android:color/white"
                  />
       </RelativeLayout>

       <android.support.v4.view.ViewPager
           android:id="@+id/my_viewpager"
           android:layout_width="wrap_content"
           android:layout_height="300dp"
           android:clipChildren="false"
           android:layout_marginLeft="50dp"
           android:layout_marginRight="50dp"
           android:layout_centerInParent="true"
           />
       <com.zhouwei.indicatorview.CircleIndicatorView
           android:id="@+id/indicatorView"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:layout_alignParentBottom="true"
           android:layout_marginBottom="60dp"
           android:layout_centerHorizontal="true"
           app:indicatorSelectColor="#C79EFE"
           app:indicatorSpace="5dp"
           app:indicatorRadius="8dp"
           app:enableIndicatorSwitch="false"
           app:indicatorTextColor="@android:color/white"
           app:fill_mode="number"
           app:indicatorColor="#C79EFE"
           />
       <TextView
           android:id="@+id/skin_desc"
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:layout_centerHorizontal="true"
           android:layout_below="@+id/my_viewpager"
           android:layout_marginTop="20dp"
           android:textColor="@android:color/white"
           android:textSize="18sp"
           />
</RelativeLayout>复制代码

2, Activity代码:

public class ViewPagerActivity extends AppCompatActivity {
    private ViewPager mViewPager;
    private VPAdapter mVPAdapter;
    private ImageView mImageBg;
    private CircleIndicatorView mCircleIndicatorView;
    private TextView mTitle,mDesc;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.viewpager_transform_layout);
        View view = findViewById(R.id.toolbar);
        StatusBarUtils.setTranslucentImageHeader(this, 0,view);
        initView();
    }

    private void initView() {
        mViewPager = (ViewPager) findViewById(R.id.my_viewpager);
        mImageBg = (ImageView) findViewById(R.id.activity_bg);
        mCircleIndicatorView = (CircleIndicatorView) findViewById(R.id.indicatorView);
        mTitle = (TextView) findViewById(R.id.title_name);
        mDesc = (TextView) findViewById(R.id.skin_desc);

        mTitle.setText("黑暗之女");
        mViewPager.setPageTransformer(false,new CustomViewPagerTransformer(this));
        // 添加监听器
        mViewPager.addOnPageChangeListener(onPageChangeListener);
        mVPAdapter = new VPAdapter(getSupportFragmentManager());
        mViewPager.setAdapter(mVPAdapter);
        mViewPager.setOffscreenPageLimit(3);
        //  Indicator 和ViewPager 建立关联
        mCircleIndicatorView.setUpWithViewPager(mViewPager);
        // 首次进入展示第二页
        mViewPager.setCurrentItem(1);


    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return mViewPager.onTouchEvent(event);
    }

    private ViewPager.OnPageChangeListener onPageChangeListener = new ViewPager.OnPageChangeListener() {
        @Override
        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {

        }

        @Override
        public void onPageSelected(int position) {
            Bitmap source = BitmapFactory.decodeResource(getResources(),VPAdapter.RES[position]);
            Bitmap bitmap = EasyBlur.with(getApplicationContext())
                    .bitmap(source)
                    .radius(20)
                    .blur();
            mImageBg.setImageBitmap(bitmap);

            mDesc.setText(mVPAdapter.getPageTitle(position));
        }

        @Override
        public void onPageScrollStateChanged(int state) {

        }
    };
}复制代码

ViewPager的每一个页面用Fragment 来展示的,Fragment代码如下:

public class ItemFragment extends Fragment {
    private PolygonView mPolygonView;
    public static ItemFragment newInstance(int resId){
        ItemFragment itemFragment = new ItemFragment();
        Bundle bundle = new Bundle();
        bundle.putInt("resId",resId);
        itemFragment.setArguments(bundle);
        return itemFragment;
    }
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.view_pager_muti_layout,null);
        mPolygonView = (PolygonView) view.findViewById(R.id.item_image);
        // 做一个属性动画
        ObjectAnimator animator = ObjectAnimator.ofFloat(mPolygonView,"rotation",0f,10f);
        animator.setDuration(10);
        animator.start();
        return view;
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        int resId = getArguments().getInt("resId");
        mPolygonView.setImageResource(resId);// 设置图片
    }
}复制代码

说明:在Fragment中对PolygonView做了一个旋转的动画,是因为PolygonView 是一个竖着的等腰梯形,但是看效果图,其实不是,还有一个小幅度的旋转,如果将这个旋转放在PolygonView 里面做的话,发现每次ViewPager 切换的时候,都有一个旋转动画,效果不好,因此将动画放在这里。应该还有其他更优雅一点的方法,有兴趣的可以去试一下。

最后

本篇文章是ViewPager 系列的第三篇文章,也是这个系列的最后一些文章,这三篇文章总结了ViewPager 的一些常用方法,如Banner 、切换动画等等。还讲了如何封装一个扩展性强,比较通用的ViewPager。这也是对自己以前用过的这些知识点的一个总结和沉淀。喜欢的话可以关注我的简书和掘金账号,会不定期的更新Android相关的优质文章。如果有什么问题的话也欢迎指出交流。Demo请访问:github.com/pinguo-zhou…

ViewPager系列文章:
ViewPager系列之 打造一个通用的ViewPager
ViewPager系列之 仿魅族应用的广告BannerView

如果你喜欢我的文章,欢迎关注我的微信公众号:Android技术杂货铺,第一时间获取有价值的Android干货文章。一起探讨Android开发技术,一起成长。
微信公众号:Android技术杂货铺

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值