Android仿知乎創(chuàng)意廣告 廣告還能這么玩?
一、概述
貌似前段時間刷知乎看到的一種非常有特色的廣告展現(xiàn)方式,即在列表頁,某一個Item顯示背后部分廣告圖,隨著列表滾動,會逐漸展示全部圖片。
剛看到的時候就想實現(xiàn)一哈,一直比較懶,公眾號后臺也有人問如何實現(xiàn),今天來給大家講解下,當然了,目前一些自定義View已經(jīng)不算難題,所以本文的講解會做一些實現(xiàn)思路引導,相信不會是那么枯燥的文章,希望對大家有一定的幫助。
恩,現(xiàn)在知乎上已經(jīng)找不到該效果了,試了多個歷史版本也沒找到,那只能貼實現(xiàn)的效果圖了~
效果圖如下:


2選1,你喜歡哪個效果圖呢~~
二、思路
好了,拋開別的,確定下本文的目標:
實現(xiàn)在列表中展示某張圖片:
- 往上滾動:在圖片剛出現(xiàn)時展示頂部部分,隨著滾動部分展示全部
- 往下滾動:在圖片剛出現(xiàn)時展示底部部分,隨著滾動部分展示全部
換句話說,我們需要在列表滾動時,改變圖片顯示的部分。
兩個點:
- 捕獲列表滾動的dy,不管是ListView還是RecyclerView相信這一點都能做到
- 圖片顯示部分變化,我們可以利用canvas.translate
結合一下,就是,監(jiān)聽列表的滾動dy,傳給我們的圖片控件,設置translate,然后繪制。
到這里,思路非常清晰,這個東西肯定能做了。
初步方案:自定義一個View,自己去繪制bitmap,對外暴露setDy(dy),然后根據(jù)dy做canvas偏移重繪即可。
有了初步方案,基本不慌了,那么再想想?
能否利用已有的控件,比如ImageView呢?
肯定可以,這樣省去了我們?nèi)ヂ暶饕粋€接受圖片的屬性,我們編寫一個子類,依然是通過設置src去使用。
那繼承ImageView實現(xiàn)一波再說。
三、實現(xiàn)
首先我們先寫個假的列表,鑒于RV用的越來越多,就用RecyclerView吧。
布局
主布局文件,一個RecyclerView即可:
- <?xml version="1.0" encoding="utf-8"?>
- <android.support.v7.widget.RecyclerView
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:id="@+id/id_recyclerview"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- />
item布局文件:
- <?xml version="1.0" encoding="utf-8"?>
- <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:background="@drawable/item_bg"
- android:gravity="center">
- <com.imooc.rvimageads.AdImageViewVersion1
- android:id="@+id/id_iv_ad"
- android:layout_width="match_parent"
- android:layout_height="180dp"
- android:scaleType="matrix"
- android:src="@mipmap/grsm"
- android:visibility="gone" />
- <TextView
- android:layout_margin="12dp"
- android:id="@+id/id_tv_title"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="這是title"
- android:textSize="16dp"
- android:textStyle="bold" />
- <TextView
- android:id="@+id/id_tv_desc"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_below="@id/id_tv_title"
- android:layout_marginLeft="12dp"
- android:layout_marginRight="12dp"
- android:layout_marginBottom="12dp"
- android:text="這是描述" />
- </RelativeLayout>
很簡單,先不用管 AdImageViewVersion1 類,這將是我們具體的實現(xiàn)類。
通過布局文件,可以看到,我們只使用了一個item布局文件,然后通過visible,gone控制展示不同形態(tài)。
Activity
- public class MainActivity extends AppCompatActivity {
- private RecyclerView mRecyclerView;
- private LinearLayoutManager mLinearLayoutManager;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- mRecyclerView = findViewById(R.id.id_recyclerview);
- List<String> mockDatas = new ArrayList<>();
- for (int i = 0; i < 100; i++) {
- mockDatas.add(i + "");
- }
- mRecyclerView.setLayoutManager(mLinearLayoutManager = new LinearLayoutManager(this));
- mRecyclerView.setAdapter(new CommonAdapter<String>(MainActivity.this,
- R.layout.item,
- mockDatas) {
- @Override
- protected void convert(ViewHolder holder, String o, int position) {
- if (position > 0 && position % 6 == 0) {
- holder.setVisible(R.id.id_tv_title, false);
- holder.setVisible(R.id.id_tv_desc, false);
- holder.setVisible(R.id.id_iv_ad, true);
- } else {
- holder.setVisible(R.id.id_tv_title, true);
- holder.setVisible(R.id.id_tv_desc, true);
- holder.setVisible(R.id.id_iv_ad, false);
- }
- }
- });
- }
僅僅是設置數(shù)據(jù)了,Adapter這里用了
- compile 'com.zhy:base-rvadapter:3.0.3'
你可以隨便用一個你自己喜歡的Adapter封裝類。
到這里,一個列表頁就顯示出來了,并且每隔6個會顯示成圖片。
不截圖了,腦補下…
現(xiàn)在才正式開始實現(xiàn)。
自定義AdImageView
- public class AdImageViewVersion1 extends AppCompatImageView {
- public AdImageViewVersion1(Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
- }
- private RectF mBitmapRectF;
- private Bitmap mBitmap;
- private int mMinDy;
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- mMinDy = h;
- Drawable drawable = getDrawable();
- if (drawable == null) {
- return;
- }
- mBitmap = drawableToBitamp(drawable);
- mBitmapRectF = new RectF(0, 0,
- w,
- mBitmap.getHeight() * w / mBitmap.getWidth());
- }
- private Bitmap drawableToBitamp(Drawable drawable) {
- if (drawable instanceof BitmapDrawable) {
- BitmapDrawable bd = (BitmapDrawable) drawable;
- return bd.getBitmap();
- }
- int w = drawable.getIntrinsicWidth();
- int h = drawable.getIntrinsicHeight();
- Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
- Canvas canvas = new Canvas(bitmap);
- drawable.setBounds(0, 0, w, h);
- drawable.draw(canvas);
- return bitmap;
- }
- // ... 省略一些代碼
- }
因為我們要繪制,所以這里我們把drawable轉(zhuǎn)成bitmap,然后我們默認要顯示***部,所以需要一個最小的偏移,即控件高度。
這些事情,我們都在onSizeChanged做了。
并且我們根據(jù)當前控件寬度,對bitmap進行了縮放,并將縮放后的尺寸存在了mBitmapRectF中,以便于繪制。
那么接下來就是繪制了,還記得繪制過程中,我們主要利用translate來控制繪制的區(qū)域,所以我們還要對外暴露一個setDy方法,so,我們的代碼大致是這樣的:
- private int mDy;
- public void setDy(int dy) {
- if (getDrawable() == null) {
- return;
- }
- mDy = dy - mMinDy;
- if (mDy <= 0) {
- mDy = 0;
- }
- if (mDy > mBitmapRectF.height() - mMinDy) {
- mDy = (int) (mBitmapRectF.height() - mMinDy);
- }
- invalidate();
- }
- @Override
- protected void onDraw(Canvas canvas) {
- if (mBitmap == null) {
- return;
- }
- canvas.save();
- canvas.translate(0, -mDy);
- canvas.drawBitmap(mBitmap, null, mBitmapRectF, null);
- canvas.restore();
- }
setDy的時候,我們做了一個邊界判斷,最小的情況,我們偏移-mMinDy,顯示圖片的底部。
***的時候,我們便宜圖片高度-mMinDy,顯示頂部部分。
所以我們對傳入的值做了最小與***值判斷。
那么在繪制的時候,就簡單了,先translate dy距離,然后繪制即可。
到這里我們的自定義View部分就結束了,代碼很少~
結合RecyclerView
接下來就是在RecyclerView滾動時,給我們傳入dy即可。
- mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
- @Override
- public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
- super.onScrolled(recyclerView, dx, dy);
- int fPos = mLinearLayoutManager.findFirstVisibleItemPosition();
- int lPos = mLinearLayoutManager.findLastCompletelyVisibleItemPosition();
- for (int i = fPos; i <= lPos; i++) {
- View view = mLinearLayoutManager.findViewByPosition(i);
- AdImageViewVersion1 adImageView = view.findViewById(R.id.id_iv_ad);
- if (adImageView.getVisibility() == View.VISIBLE) {
- adImageView.setDy(mLinearLayoutManager.getHeight() - view.getTop());
- }
- }
- }
- });
通過addOnScrollListener監(jiān)聽,當滾動時,拿到所有可見的Item,找出正在顯示圖片的Item。然后調(diào)用setDy,dy的值為 mLinearLayoutManager.getHeight() - view.getTop() ,當View從***部出現(xiàn)的時候為0,當View到達最頂部的時候為當前rv的高度。
你可以合理的利用setDy傳入的值,做移動差,顯示區(qū)域從上到下等,都可以。
這樣就完成了~~

一句話實現(xiàn):即滾動時不斷改變dy,然后translate繪制即可。
四、再想想
看著這個代碼,好像 drawableToBitamp 看起來非常不爽,也是比較耗內(nèi)存的部分。我們再想想:
本身Drawable就是能繪制的,為什么我們要轉(zhuǎn)成bitmap呢?
好像有道理,ImageView本身繪制的就是Drawable,我們需要控制的就是這個Drawable的繪制范圍要足夠大,不能被控件本身的寬高所影響,導致圖片被壓扁。
好像有那么一個方法:
- drawable.setBounds();
那就簡單了,去除drawable2bitmap的代碼,直接利用原本的繪制即可,我們唯一要做的就是設置bounds,做一個translate dy即可。
完整代碼:
- public class AdImageView extends AppCompatImageView {
- // 刪除構造方法
- private int mDx;
- private int mMinDx;
- public void setDx(int dx) {
- if (getDrawable() == null) {
- return;
- }
- mDx = dx - mMinDx;
- if (mDx <= 0) {
- mDx = 0;
- }
- if (mDx > getDrawable().getBounds().height() - mMinDx) {
- mDx = getDrawable().getBounds().height() - mMinDx;
- }
- invalidate();
- }
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- mMinDx = h;
- }
- public int getDx() {
- return mDx;
- }
- @Override
- protected void onDraw(Canvas canvas) {
- Drawable drawable = getDrawable();
- int w = getWidth();
- int h = (int) (getWidth() * 1.0f / drawable.getIntrinsicWidth() * drawable.getIntrinsicHeight());
- drawable.setBounds(0, 0, w, h);
- canvas.save();
- canvas.translate(0, -getDx());
- super.onDraw(canvas);
- canvas.restore();
- }
- }
短短的代碼就實現(xiàn)了,這樣看起來順眼多了~~
再貼下效果圖:

效果圖主要看字,你懂的!
好了,本篇總結:
- 看到當看一個效果,可以先對它進行拆分,找出關鍵點,針對每個關鍵點,考慮可行性。
- 如果確定每個點都可行,那么基本的方案就出來了。
- 有了基本的方案,不要著急寫,再想想還有無改善空間。
例子比較簡單,have a nice day ~~