ViewPager Series-Skin Browsing Effect Imitating Palm Heroes Alliance

Keywords: Android Fragment Attribute github


Cover chart. png

Having a weekend off is a happy thing for programmers. After a week's hard work, I can finally relax and relax. As a LOL enthusiast, the happiest thing on the weekend is, of course, to have a few small partners together in the dark. It's also a great weekend pleasure to be super-spiritual and kneeling together. In recent days, the hero League has been conducting activities, and draws an Anne to limit the skin, which makes me happy, so I immediately landed in the palm League client to check the skin. After entering the Skin Browsing Interface, I feel that this Skin Browsing effect is really good, as follows:


Palmar Skin Browsing Effect.gif

As a programmer, of course, the first thing to think about is how it works. How can I achieve similar results? So it took half a day to do a similar effect. So this article will share how to achieve this effect. The final results are as follows:


Skin Browsing Effect of Palm-like Alliance.gif

Thoughts and Analysis

Before we start coding, let's analyze the interface elements and what technology we use to implement the various parts.

1. First of all, the sliding of the whole interface, which we can see at a glance, is implemented with ViewPager.
2. ViewPager has zoom-in and zoom-out animation when sliding. It's easy to do with ViewPager.Transfoemer.
3. ViewPager displays multiple pages (parts of the front and back pages).
4. The shape of the interface picture, the isosceles trapezoid with 90 degrees rotation. This can only be achieved with a custom View.
5. The background of the whole interface is the Gauss blurred image of the current picture.

code implementation

The components of the interface are analyzed above. Now let's look at the implementation.

1. ViewPager shows multiple pages
This problem, which we discussed in our previous article, will not be repeated here. It is to use the clipChildren attribute of ViewGroup, which has a value of false. That is to add the following line of code to the node in the whole layout:

android:clipChildren="false"

Then, the ViewPager needs to set the left and right margins, which is where the front and back pages are displayed.

<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"
           />

As can be seen from the effect diagram above, there is a gap between the current page and the front and back pages. We only need to add margin attributes around the Item layout:

 android:layout_marginLeft="30dp"
 android:layout_marginRight="30dp"

Okay, so the ViewPager can display multiple pages, and there is a certain distance between the current page and the front and back pages.

2. Animation when ViewPager switches
Custom animation for ViewPager switching uses ViewPager.PageTransformer, which was also mentioned in the previous article, but I haven't seen it before. I'm not going to go into details here. I'm going to code it directly.

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) {
        // The possible value of position is, in fact, as can be seen from the annotations of the official examples:
        //[-Infinity, -1] No longer visible
        // (1,+Infinity] is no longer visible
        // [-1,1]
        // From the point of writing Page Transformer, the focus area of View animation is [-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 And Pixel Conversion
     */
    private int dp2px(Context context, float dipValue) {
        float m = context.getResources().getDisplayMetrics().density;
        return (int) (dipValue * m + 0.5f);
    }
}

3. Custom Polygon ImageView
The polygon ImageView, which we implement in a custom way, inherits the ImageView, and then overrides the onDraw() method. There are two ways to implement this irregular polygon View. First, using PorterDuffXfermode, this method requires you to give a mask image. In onDraw method, first draw the mask image, then set Paint's setXfermode to PorterDuff.Mode.SRC_IN, and then draw the picture to be displayed, so that the displayed image can be cut into the shape of the mask. Secondly, using clipPath() method of canvas, we use Path to draw polygons, then clipPath() cuts the canvas into drawn shape, and then draws the pictures to be displayed.

For more usage of PorterDuffXfermode, you can go to google if you are interested. There are many related articles on the internet. Here I use a combination of the two methods. First I use clipPath to get a bitmap that needs shape, and then I use PorterDuffXfermode. The custom View code is as follows:

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);// Close Hardware Acceleration
        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);//Anti-Aliasing
        mBorderPaint.setDither(true);//debounce

        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;
    }

    /**
     * Equivalent clipping of the original map
     */
    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);
        }
    }

}

Suggestion: When using clipPath method here, there will be a lot of sawtooth, even if Paint set anti-sawtooth is useless, so it is recommended to use PorterDuffXfermode method. To achieve similar results, it is best to find a designer to ask for a mask shape map. Implemented in PorterDuffXfermode, it is simple and effective.

Through the above three steps, in fact, the effect of the entire interface has almost come out, and finally we need to do is the Gauss blurred background image.

4. Gauss blurring of background image
Background Gaussian blurring is very simple. I have written about the comparison of several Gaussian blurring methods before.( Gauss Blur Solution for Android Pictures Finally, a convenient library is encapsulated( https://github.com/pinguo-zhouwei/EasyBlur Just a few lines of code. In view Pager's onPage Select method, we get the displayed pictures and process them with Gauss blur.

      @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));
        }

Finally, the complete layout file and Activity code are given.

1. The activity layout file:

<?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">
       <!-- Gauss Fuzzy Background-->
       <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 code:

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("Dark Daughter");
        mViewPager.setPageTransformer(false,new CustomViewPagerTransformer(this));
        // Adding listeners
        mViewPager.addOnPageChangeListener(onPageChangeListener);
        mVPAdapter = new VPAdapter(getSupportFragmentManager());
        mViewPager.setAdapter(mVPAdapter);
        mViewPager.setOffscreenPageLimit(3);
        //  Connection between Indicator and ViewPager
        mCircleIndicatorView.setUpWithViewPager(mViewPager);
        // Enter the second page of the exhibition for the first time
        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) {

        }
    };
}

Each page of the ViewPager is shown in Fragment, the Fragment code is as follows:

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);
        // Make an attribute animation
        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);// Setting Pictures
    }
}

Explanation: PolygonView is a rotating animation in Fragment because PolygonView is a vertical isosceles trapezoid, but look at the effect map, actually not, there is a small rotation. If you put this rotation in PolygonView, you will find that every time the ViewPager switch, there is a rotating animation, the effect is not good, so the animation will be put. Ad locum. There should be other more elegant ways, if you are interested, you can try them.

Last

This article is the third and final article in the ViewPager series. These three articles summarize some common methods of ViewPager, such as Banner, switching animation, and so on. It also describes how to encapsulate a more extensible and general ViewPager. This is also a summary and precipitation of these knowledge points that I have used before. If you like, you can pay attention to my brief book and Nuggets account, and update Android-related high-quality articles from time to time. If you have any questions, you are welcome to point out the exchange. Demo please visit: https://github.com/pinguo-zhouwei/AndroidTrainingSimples

ViewPager series:
The ViewPager Series Creates a Universal ViewPager
BannerView: An Advertising Banner View for the Imitation of Meizu Apps in ViewPager Series

If you like my article, please pay attention to my Wechat Public Number: Android Technology Grocery Store, get valuable Android dry articles for the first time. Explore Android development technology together and grow together.

Posted by fantic on Sat, 22 Jun 2019 11:16:31 -0700