Can Android imitate Zhihu's creative advertising be played like this?

Can Android imitate Zhihu's creative advertising be played like this?

1. Overview

It seems that I saw a very unique way of advertising display when I was browsing Zhihu some time ago. That is, on the list page, a certain item shows part of the advertising picture behind it, and as the list scrolls, all the pictures will gradually be displayed.

When I first saw it, I wanted to implement it, but I have always been lazy. Some people also asked me how to implement it in the background of the official account. Today I will explain it to you. Of course, some custom Views are not difficult at present, so the explanation in this article will provide some guidance on implementation ideas. I believe it will not be such a boring article. I hope it will be of some help to you.

Well, I can’t find this effect on Zhihu anymore. I tried multiple historical versions but still couldn’t find it. So I can only post the effect picture of the implementation~

The effect diagram is as follows:

Choose 1 from 2, which rendering do you like?

2. Ideas

Okay, let’s put aside other things and determine the goal of this article:

To display a picture in a list:

  • Scroll up: Show the top part of the image when it first appears, and show the whole image as you scroll.
  • Scroll down: Show the bottom part of the image when it first appears, and show the whole part as you scroll.

In other words, we need to change the displayed part of the image when the list scrolls.

Two points:

  • Capture the dy of the list scrolling, whether it is ListView or RecyclerView, I believe this can be done
  • The picture shows some changes, we can use canvas.translate

To combine them, we listen to the scrolling dy of the list, pass it to our image control, set the translate, and then draw it.

At this point, the idea is very clear, and this thing can definitely be done.

Preliminary plan: customize a View, draw the bitmap yourself, expose setDy(dy) to the outside world, and then redraw the canvas based on dy offset.

Now that I have a preliminary plan, I am basically not panicking anymore. So what should I think about?

Can we use existing controls, such as ImageView?

Of course you can. This saves us from declaring a property to accept the image. We write a subclass and still use it by setting src.

Let’s inherit ImageView and implement it first.

Implementation

First, let's write a fake list. Since RV is used more and more, let's use RecyclerView.

layout

The main layout file, a RecyclerView is enough:

  1. <?xml version= "1.0" encoding= "utf-8" ?>
  2. <android.support.v7.widget.RecyclerView
  3. xmlns:android= "http://schemas.android.com/apk/res/android"  
  4. xmlns:app= "http://schemas.android.com/apk/res-auto"  
  5. android:id= "@+id/id_recyclerview"  
  6. android:layout_width= "match_parent"  
  7. android:layout_height= "match_parent"  
  8. />

Item layout file:

  1. <?xml version= "1.0" encoding= "utf-8" ?>
  2. <RelativeLayout xmlns:android= "http://schemas.android.com/apk/res/android"  
  3. android:layout_width= "match_parent"  
  4. android:layout_height= "wrap_content"  
  5. android:background= "@drawable/item_bg"  
  6. android:gravity= "center" >
  7.  
  8. <com.imooc.rvimageads.AdImageViewVersion1
  9. android:id= "@+id/id_iv_ad"  
  10. android:layout_width= "match_parent"  
  11. android:layout_height= "180dp"  
  12. android:scaleType= "matrix"  
  13. android:src= "@mipmap/grsm"  
  14. android:visibility= "gone" />
  15.  
  16. <TextView
  17. android:layout_margin= "12dp"  
  18. android:id= "@+id/id_tv_title"  
  19. android:layout_width= "wrap_content"  
  20. android:layout_height= "wrap_content"  
  21. android:text= "This is the title"  
  22. android:textSize= "16dp"  
  23. android:textStyle= "bold" />
  24.  
  25. <TextView
  26. android:id= "@+id/id_tv_desc"  
  27. android:layout_width= "wrap_content"  
  28. android:layout_height= "wrap_content"  
  29. android:layout_below= "@id/id_tv_title"  
  30. android:layout_marginLeft= "12dp"  
  31. android:layout_marginRight= "12dp"  
  32. android:layout_marginBottom= "12dp"  
  33. android:text= "This is a description" />
  34.  
  35. </RelativeLayout>

Very simple, ignore the AdImageViewVersion1 class for now, this will be our specific implementation class.

From the layout file, you can see that we only use one item layout file, and then use visible and gone controls to display different forms.

Activity

  1. public class MainActivity extends AppCompatActivity {
  2.  
  3. private RecyclerView mRecyclerView;
  4. private LinearLayoutManager mLinearLayoutManager;
  5.  
  6. @Override
  7. protected void onCreate(Bundle savedInstanceState) {
  8. super.onCreate(savedInstanceState);
  9. setContentView(R.layout.activity_main);
  10.  
  11. mRecyclerView = findViewById(R.id.id_recyclerview);
  12.  
  13. List<String> mockDatas = new ArrayList<>();
  14. for ( int i = 0; i < 100; i++) {
  15. mockDatas.add (i + "" );
  16. }
  17.  
  18. mRecyclerView.setLayoutManager(mLinearLayoutManager = new LinearLayoutManager(this));
  19.  
  20. mRecyclerView.setAdapter(new CommonAdapter<String>(MainActivity.this,
  21. R.layout.item,
  22. mockDatas) {
  23. @Override
  24. protected void convert (ViewHolder holder, String o, int position) {
  25. if (position > 0 && position % 6 == 0) {
  26. holder.setVisible(R.id.id_tv_title, false );
  27. holder.setVisible(R.id.id_tv_desc, false );
  28. holder.setVisible(R.id.id_iv_ad, true );
  29. } else {
  30. holder.setVisible(R.id.id_tv_title, true );
  31. holder.setVisible(R.id.id_tv_desc, true );
  32. holder.setVisible(R.id.id_iv_ad, false );
  33. }
  34. }
  35. });
  36. }

Just set the data, Adapter is used here

  1. compile 'com.zhy:base-rvadapter:3.0.3'  

You can use any Adapter encapsulation class you like.

At this point, a list page is displayed, and every 6 items will be displayed as pictures.

No more screenshots, just imagine...

Now it is officially starting to be realized.

Custom AdImageView

  1. public class AdImageViewVersion1 extends AppCompatImageView {
  2. public AdImageViewVersion1(Context context, @Nullable AttributeSet attrs) {
  3. super(context, attrs);
  4. }
  5.  
  6. private RectF mBitmapRectF;
  7. private Bitmap mBitmap;
  8.  
  9. private int mMinDy;
  10.  
  11. @Override
  12. protected void onSizeChanged( int w, int h, int oldw, int oldh) {
  13. super.onSizeChanged(w, h, oldw, oldh);
  14.  
  15. mMinDy = h;
  16. Drawable drawable = getDrawable();
  17.  
  18. if (drawable == null ) {
  19. return ;
  20. }
  21.  
  22. mBitmap = drawableToBitamp(drawable);
  23. mBitmapRectF = new RectF(0, 0,
  24. w,
  25. mBitmap.getHeight() * w / mBitmap.getWidth());
  26.  
  27. }
  28.  
  29.  
  30. private Bitmap drawableToBitamp(Drawable drawable) {
  31. if (drawable instanceof BitmapDrawable) {
  32. BitmapDrawable bd = (BitmapDrawable) drawable;
  33. return bd.getBitmap();
  34. }
  35. int w = drawable.getIntrinsicWidth();
  36. int h = drawable.getIntrinsicHeight();
  37. Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
  38. Canvas canvas = new Canvas(bitmap);
  39. drawable.setBounds(0, 0, w, h);
  40. drawable.draw(canvas);
  41. return bitmap;
  42. }
  43.  
  44. // ... some code omitted
  45. }

Because we want to draw, we convert the drawable into a bitmap here, and then we display the top by default, so we need a minimum offset, which is the height of the control.

We have done all these things in onSizeChanged.

And we scale the bitmap according to the current control width, and store the scaled size in mBitmapRectF for easy drawing.

Then the next step is drawing. Remember that during the drawing process, we mainly use translate to control the drawing area, so we also need to expose a setDy method to the outside world. So, our code is roughly like this:

  1. private int mDy;
  2.  
  3. public void setDy( int dy) {
  4.  
  5. if (getDrawable() == null ) {
  6. return ;
  7. }
  8. mDy = dy - mMinDy;
  9. if (mDy <= 0) {
  10. mDy = 0;
  11. }
  12. if (mDy > mBitmapRectF.height() - mMinDy) {
  13. mDy = ( int ) (mBitmapRectF.height() - mMinDy);
  14. }
  15. invalidate();
  16. }
  17.  
  18. @Override
  19. protected void onDraw(Canvas canvas) {
  20. if (mBitmap == null ) {
  21. return ;
  22. }
  23. canvas.save();
  24. canvas.translate(0, -mDy);
  25. canvas.drawBitmap(mBitmap, null , mBitmapRectF, null );
  26. canvas.restore();
  27. }

When setting Dy, we make a boundary judgment. In the minimum case, we offset -mMinDy to display the bottom of the picture.

***When we use the image height -mMinDy, we show the top part.

So we make a minimum and maximum value judgment on the passed value.

Then when drawing, it is simple, first translate the dy distance, and then draw.

Our custom View part ends here, with very little code~

Combine with RecyclerView

The next step is to pass dy to us when RecyclerView scrolls.

  1. mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
  2. @Override
  3. public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
  4. super.onScrolled(recyclerView, dx, dy);
  5.  
  6. int fPos = mLinearLayoutManager.findFirstVisibleItemPosition();
  7. int lPos = mLinearLayoutManager.findLastCompletelyVisibleItemPosition();
  8. for ( int i = fPos; i <= lPos; i++) {
  9. View   view = mLinearLayoutManager.findViewByPosition(i);
  10. AdImageViewVersion1 adImageView = view .findViewById(R.id.id_iv_ad);
  11. if (adImageView.getVisibility() == View .VISIBLE) {
  12. adImageView.setDy(mLinearLayoutManager.getHeight() - view .getTop());
  13. }
  14. }
  15. }
  16. });

Through addOnScrollListener listening, when scrolling, get all visible items and find the item that is displaying the picture. Then call setDy, the value of dy is mLinearLayoutManager.getHeight() - view.getTop(), when the view appears from the top, it is 0, when the view reaches the top, it is the height of the current rv.

You can reasonably use the value passed in by setDy to make a movement difference, display the area from top to bottom, etc.

That's it~~

To achieve it in one sentence: keep changing dy when scrolling, and then translate to draw it.

4. Think Again

Looking at this code, it seems that drawableToBitamp looks very unpleasant and is also the part that consumes more memory. Let's think about it again:

Drawable itself can be drawn, why do we need to convert it into a bitmap?

It seems to make sense. ImageView itself draws Drawable. What we need to control is that the drawing range of this Drawable should be large enough and cannot be affected by the width and height of the control itself, causing the image to be flattened.

There seems to be a method:

  1. drawable.setBounds();

That's easy. Just remove the drawable2bitmap code and use the original drawing directly. The only thing we have to do is to set the bounds and do a translate dy.

Complete code:

  1. public class AdImageView extends AppCompatImageView {
  2. // Delete the constructor
  3.  
  4. private int mDx;
  5. private int mMinDx;
  6.  
  7. public void setDx( int ​​dx) {
  8. if (getDrawable() == null ) {
  9. return ;
  10. }
  11. mDx = dx - mMinDx;
  12. if (mDx <= 0) {
  13. mDx = 0;
  14. }
  15. if (mDx > getDrawable().getBounds().height() - mMinDx) {
  16. mDx = getDrawable().getBounds().height() - mMinDx;
  17. }
  18. invalidate();
  19. }
  20.  
  21. @Override
  22. protected void onSizeChanged( int w, int h, int oldw, int oldh) {
  23. super.onSizeChanged(w, h, oldw, oldh);
  24. mMinDx = h;
  25. }
  26.  
  27. public   int getDx() {
  28. return mDx;
  29. }
  30.  
  31. @Override
  32. protected void onDraw(Canvas canvas) {
  33.  
  34. Drawable drawable = getDrawable();
  35. int w = getWidth();
  36. int h = ( int ) (getWidth() * 1.0f / drawable.getIntrinsicWidth() * drawable.getIntrinsicHeight());
  37. drawable.setBounds(0, 0, w, h);
  38. canvas.save();
  39. canvas.translate(0, -getDx());
  40. super.onDraw(canvas);
  41. canvas.restore();
  42. }
  43. }

It is achieved with just a short code, which looks much more pleasing to the eye~~

Here is the effect diagram:

The effect picture mainly depends on the words, you know!

Well, this article summarizes:

  • When you see an effect, you can first break it down, find the key points, and consider the feasibility for each key point.
  • If you determine that every point is feasible, then the basic plan will emerge.
  • Once you have a basic plan, don't rush to write it. Think about whether there is room for improvement.

The example is relatively simple, have a nice day ~~

<<:  Spam SMS filtering app based on iOS 11 machine learning: Pandas eat SMS

>>:  iOS 11 installation rate has reached 59%, but it is lower than iOS 10 during the same period

Recommend

What are the differences among the various types of milk?

I heard that yogurt is better than pure milk, is ...

Why are older people more vulnerable to online rumors?

Nowadays, we play with our phones every day, and ...

How to quickly build a data operation and analysis system?

Regarding the question of how to quickly build a ...

In Microsoft's eyes, XBOX is actually such a role...

There is no doubt that in the battle for the next...

Do rattlesnakes have a pair of "eyes" that can sense heat sources?

Do you know about the Sidewinder missile? In the ...