ViewPager系列之-仿掌上英雄聯盟皮膚瀏覽效果

依然範特稀西發表於2019-03-02

能有一個雙休的週末,對於程式設計師來說,也算是一件幸福的事情吧。苦逼的加了一週的班,終於可以休息放鬆放鬆了。作為一個LOL愛好者,週末最開心的事當然就是約上幾個小夥伴一起開黑了。一起超神、一起連跪,也算是週末的一大樂事。這幾天英雄聯盟搞活動,抽到一個安妮限定皮膚,可把我樂壞了,於是馬上就登陸掌盟客戶端檢視皮膚。進入皮膚瀏覽介面之後,覺得這個皮膚瀏覽的效果還真不錯,如下圖:

ViewPager系列之-仿掌上英雄聯盟皮膚瀏覽效果
掌盟皮膚 瀏覽效果.gif

作為一個程式設計師,當然第一時間就是思考它是怎麼實現的?我能用什麼方法來實現類似的效果?於是花了半天的時間,做了一個類似的效果。因此本篇文章就分享一下如何實現這一效果。最後實現的效果如下:

ViewPager系列之-仿掌上英雄聯盟皮膚瀏覽效果
仿掌盟皮膚 瀏覽效果.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技術雜貨鋪

ViewPager系列之-仿掌上英雄聯盟皮膚瀏覽效果

相關文章