中国领先的IT技术网站
|
|

Android仿知乎创意广告 广告还能这么玩?

貌似前段时间刷知乎看到的一种非常有特色的广告展现方式,即在列表页,某一个Item显示背后部分广告图,随着列表滚动,会逐渐展示全部图片。今天来给大家讲解下,当然了,目前一些自定义View已经不算难题,所以本文的讲解会做一些实现思路引导,相信不会是那么枯燥的文章,希望对大家有一定的帮助。

作者:佚名来源:Hongyang|2017-12-06 15:14

开发者大赛路演 | 12月16日,技术创新,北京不见不散


一、概述

貌似前段时间刷知乎看到的一种非常有特色的广告展现方式,即在列表页,某一个Item显示背后部分广告图,随着列表滚动,会逐渐展示全部图片。

刚看到的时候就想实现一哈,一直比较懒,公众号后台也有人问如何实现,今天来给大家讲解下,当然了,目前一些自定义View已经不算难题,所以本文的讲解会做一些实现思路引导,相信不会是那么枯燥的文章,希望对大家有一定的帮助。

恩,现在知乎上已经找不到该效果了,试了多个历史版本也没找到,那只能贴实现的效果图了~

效果图如下:

2选1,你喜欢哪个效果图呢~~

二、思路

好了,抛开别的,确定下本文的目标:

实现在列表中展示某张图片:

  • 往上滚动:在图片刚出现时展示顶部部分,随着滚动部分展示全部
  • 往下滚动:在图片刚出现时展示底部部分,随着滚动部分展示全部

换句话说,我们需要在列表滚动时,改变图片显示的部分。

两个点:

  • 捕获列表滚动的dy,不管是ListView还是RecyclerView相信这一点都能做到
  • 图片显示部分变化,我们可以利用canvas.translate

结合一下,就是,监听列表的滚动dy,传给我们的图片控件,设置translate,然后绘制。

到这里,思路非常清晰,这个东西肯定能做了。

初步方案:自定义一个View,自己去绘制bitmap,对外暴露setDy(dy),然后根据dy做canvas偏移重绘即可。

有了初步方案,基本不慌了,那么再想想?

能否利用已有的控件,比如ImageView呢?

肯定可以,这样省去了我们去声明一个接受图片的属性,我们编写一个子类,依然是通过设置src去使用。

那继承ImageView实现一波再说。

三、实现

首先我们先写个假的列表,鉴于RV用的越来越多,就用RecyclerView吧。

布局

主布局文件,一个RecyclerView即可:

  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布局文件:

  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="这是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="这是描述" /> 
  34.  
  35. </RelativeLayout> 

很简单,先不用管 AdImageViewVersion1 类,这将是我们具体的实现类。

通过布局文件,可以看到,我们只使用了一个item布局文件,然后通过visible,gone控制展示不同形态。

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

仅仅是设置数据了,Adapter这里用了

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

你可以随便用一个你自己喜欢的Adapter封装类。

到这里,一个列表页就显示出来了,并且每隔6个会显示成图片。

不截图了,脑补下…

现在才正式开始实现。

自定义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.     // ... 省略一些代码 

因为我们要绘制,所以这里我们把drawable转成bitmap,然后我们默认要显示最底部,所以需要一个最小的偏移,即控件高度。

这些事情,我们都在onSizeChanged做了。

并且我们根据当前控件宽度,对bitmap进行了缩放,并将缩放后的尺寸存在了mBitmapRectF中,以便于绘制。

那么接下来就是绘制了,还记得绘制过程中,我们主要利用translate来控制绘制的区域,所以我们还要对外暴露一个setDy方法,so,我们的代码大致是这样的:

  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. @Override 
  18. protected void onDraw(Canvas canvas) { 
  19.     if (mBitmap == null) { 
  20.         return
  21.     } 
  22.     canvas.save(); 
  23.     canvas.translate(0, -mDy); 
  24.     canvas.drawBitmap(mBitmap, null, mBitmapRectF, null); 
  25.     canvas.restore(); 

setDy的时候,我们做了一个边界判断,最小的情况,我们偏移-mMinDy,显示图片的底部。

最大的时候,我们便宜图片高度-mMinDy,显示顶部部分。

所以我们对传入的值做了最小与最大值判断。

那么在绘制的时候,就简单了,先translate dy距离,然后绘制即可。

到这里我们的自定义View部分就结束了,代码很少~

结合RecyclerView

接下来就是在RecyclerView滚动时,给我们传入dy即可。

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

通过addOnScrollListener监听,当滚动时,拿到所有可见的Item,找出正在显示图片的Item。然后调用setDy,dy的值为 mLinearLayoutManager.getHeight() - view.getTop() ,当View从最底部出现的时候为0,当View到达最顶部的时候为当前rv的高度。

你可以合理的利用setDy传入的值,做移动差,显示区域从上到下等,都可以。

这样就完成了~~

一句话实现:即滚动时不断改变dy,然后translate绘制即可。

四、再想想

看着这个代码,好像 drawableToBitamp 看起来非常不爽,也是比较耗内存的部分。我们再想想:

本身Drawable就是能绘制的,为什么我们要转成bitmap呢?

好像有道理,ImageView本身绘制的就是Drawable,我们需要控制的就是这个Drawable的绘制范围要足够大,不能被控件本身的宽高所影响,导致图片被压扁。

好像有那么一个方法:

  1. drawable.setBounds(); 

那就简单了,去除drawable2bitmap的代码,直接利用原本的绘制即可,我们唯一要做的就是设置bounds,做一个translate dy即可。

完整代码:

  1. public class AdImageView extends AppCompatImageView { 
  2.     // 删除构造方法 
  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.     } 

短短的代码就实现了,这样看起来顺眼多了~~

再贴下效果图:

效果图主要看字,你懂的!

好了,本篇总结:

  • 看到当看一个效果,可以先对它进行拆分,找出关键点,针对每个关键点,考虑可行性。
  • 如果确定每个点都可行,那么基本的方案就出来了。
  • 有了基本的方案,不要着急写,再想想还有无改善空间。

例子比较简单,have a nice day ~~

【编辑推荐】

  1. 谷歌宣布12月正式发布Android 8.1 安卓8.0适配厂商需加速
  2. 随手记 Android 沉浸式状态栏的踩坑之路
  3. Android 软键盘的显示和隐藏,这样操作就对了
  4. Android Go系统来了,世界“功能机”将成历史
  5. 为开发低端市场 谷歌在印度推出简化版Android系统
【责任编辑:未丽燕 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

读 书 +更多

Oracle 10g应用指导与案例精讲

本书作者结合自己多年实践经验,从Oracle开发应用中遇到的问题着手,全面系统地介绍Oracle的安装与卸载、数据字典、安全管理以及用PL/SQL开...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊