Android知乎廣告效果

達峰a發表於2018-01-22

知乎的廣告效果一直想寫,無奈最近才有時間。 先看效果:

效果.gif
肯定要自定義view了,一個類似imageView的控制元件,還要給它一個值用來指定廣告圖片的顯示位置。

問題:

1.圖片如何在範圍內(單個item範圍)上下移動,如窗戶一般,後面的圖是可以動的,但是窗戶是固定的。
2.圖片移動的時機肯定和recycleView滾動監聽item有關,用哪些方法?

解決:

1.窗戶問題首先想到imageView的scaleType屬性,而scaleType中只有matrix和center可以在不縮放圖片的情況下顯示一張大圖中的部分,center始終顯示在圖片中間部分,不符合要求;matrix不指定顯示位置,合適。
2.recycleView Item的滾動監聽,剛好前段時間在仿寫微博視訊自動播放時接觸過,recycleView提供了一些譬如FindFirstVisibleItemPosition(當前螢幕第一個item的position),FindFirstCompletelyVisibleItemPosition(當前螢幕第一個完全顯示item的position)等方法,可以利用這些方法,把當前的item找到,再利用instanceof關鍵字比較當前item是不是我的廣告item,如果是再想辦法讓廣告圖片動起來。

步驟:

1.自定義一個廣告imageView,把他變成窗戶:

繼承imageView,只需要重寫他的2個方法,onSizeChanged和onDraw。
onSizeChanged用來得到控制元件高度
onDraw移動廣告圖片

int itemHeight = 0;    //自定義imageView高度
private float rate = 1;  //初始化顯示比率

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    itemHeight = h; //廣告item的高度
}

@Override
protected void onDraw(Canvas canvas) {
    Drawable drawable = getDrawable();
    if (drawable == null) {
        return;
    }
    int w = getWidth();
    int h = (int) (getWidth() * 1.0f / drawable.getIntrinsicWidth() * drawable.getIntrinsicHeight());
    drawable.setBounds(0, 0, w, h);//設定圖片顯示的絕對範圍
    int maxDy = h - itemHeight; //圖片可以移動的最大距離為(圖片有效移動距離):   (0 ~ -maxDy)
    canvas.save();
    canvas.translate(0, -rate * maxDy);
    super.onDraw(canvas);
    canvas.restore();
}



public void setDy(int itemDy, int rvheight) {
    int allHeight = rvheight - itemHeight;  //有效滑動高度(廣告有效移動距離)
    rate = itemDy * 1f / allHeight;
    
    if (rate <= 0) {
        rate = 0;
    }
    if (rate >= 1) {
        rate = 1;
    }
    invalidate();
}
複製程式碼

setDy方法可以先不管。
onDraw中說幾個點:

  • super.onDraw(canvas)程式碼中的位置
    super.onDraw(canvas)是實現原本imageView邏輯的地方,涉及自定義view繪製先後問題;假如我用canvas畫了一個圓,畫圓程式碼寫在super之前: 這個圓會先繪製出來,再走super,就會出現imageView把圓擋住的情況,畫圓程式碼寫在super之後: 先走super再畫圓,圓就在imageView的上面。參考上面程式碼中的super位置,先把圖片的位置通過 canvas.translate方法移動之後,再利用super原本邏輯繪製出圖片,就實現圖片在視窗中移動的效果了。 (此番解釋只針對繼承已有的imageview,textview等,如果是繼承View,super位置就很隨意了,因為super是個空實現)
  • drawable.setBounds(l,t,r,b)方法
    這個方法給圖片設定一個絕對位置範圍(或者說相對螢幕的顯示範圍),上面程式碼中的範圍計算(引數r,b)其實就是 整個螢幕除開狀態列導航欄以外的範圍(recycleView的範圍)。 int w = getWidth()算出圖片可以顯示的最大寬度,再通過最大寬度 / 圖片原本寬度 = 最大高度 / 圖片原本高度 計算出最大高度 h。也就是int h = ....這一句。

通過onDraw方法,已經可以實現:一個imageView控制元件,動態的去移動它的內部圖片。這個自定義的imageView就算是完成了。

2.獲取recycleView監聽以及位置計算

寫監聽之前想想如何把recycleView的item與自定義imageView聯絡起來,通過 canvas.translate(dx,dy)讓圖片動起來,必須要求出dy: 可以看看效果,只要廣告的item有一點不在螢幕內,那麼其中的圖片是不會移動的,那麼我們廣告item有效移動距離就是整個recycleView的高度減去廣告item的高度,如圖綠色線:

廣告item有效距離.jpg

而我們自定義imageView中圖片有效移動距離是整個圖片的高度減去視窗的高度,如圖綠色線:(紅色框就相當於自定義imageView視窗,整張圖就是窗後可以translate的圖片)

圖片有效移動距離.jpg

關係就出來了: 廣告item位置 / 廣告有效移動距離 = dy / 圖片有效移動距離

重寫RecyclerView.OnScrollListener中的onScrolled方法,我們要得到:廣告item位置 和 廣告有效移動距離

@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
    super.onScrolled(recyclerView, dx, dy);

    int first = layoutManager.findFirstCompletelyVisibleItemPosition(); //第一個完全顯示的item
    int last = layoutManager.findLastCompletelyVisibleItemPosition(); //最後一個完全顯示的item
    int firstPosition = layoutManager.findFirstVisibleItemPosition(); //第一個顯示的item
    int lastPosition = layoutManager.findLastVisibleItemPosition(); //最後一個顯示的item 
    
    //迴圈遍歷當前螢幕中顯示的所有item
    for (int i = firstPosition; i <= lastPosition; i++) {
        RecyclerView.ViewHolder viewHolder = recyclerView.findViewHolderForAdapterPosition(i);
        //找出螢幕中的廣告item
        if (viewHolder instanceof TxRecycleAdapter.ZhiHuHolder) {
            TxRecycleAdapter.ZhiHuHolder zhiHuHolder = (TxRecycleAdapter.ZhiHuHolder) viewHolder;
            View itemView = zhiHuHolder.itemView;
            //獲取到廣告item的位置 (item的頂部 與 recycleView頂部的距離)
            int top = itemView.getTop();
            //獲取recycleView的高度
            int height = recyclerView.getHeight();
            //呼叫自定義imageView中的方法,實現圖片的移動
            zhiHuHolder.adImageView.setDy(top, height);
        }
    }
}
複製程式碼

int top = itemView.getTop(); top = 廣告item位置;
廣告有效移動距離 = recycleView的高度 - 廣告item的高度,這一點的實現放在了自定義imageView的setDy方法中。

注意方法中的for迴圈

for (int i = firstPosition; i <= lastPosition; i++) {}

使用的是firstPositionlastPosition,也就是//第一個顯示的item和//最後一個顯示的item。 知乎廣告的效果是 廣告item 完全顯示才會去translate圖片,一開始我使用的是firstlast,他們正好也是完全顯示的意思,for迴圈少走一兩次挺好的;但是在驗證效果的時候發現一個尷尬的問題:recycleView滑動速度過快,會出現廣告item不能translate至底部或者頂部的情況;日誌監視了一下,原因就是滑動過快。在程式碼中的表現是什麼呢?自定義imageView中setDy()方法的rate變化異常。

rate等於1圖片剛好顯示在 頂部
rate等於0圖片剛好顯示在 底部
rate從0~1:
滑動慢 rate可能是這麼變化的:0.05, 0.10,0.15,0.20 .....,0.80,0.85,0.90,0.95,1.0。
滑動快 rate可能是這麼變化的:0.3,0.6,0.9。
壓根就不會等於1或者等於0,那圖片的translate位置肯定就不對了。

出現這個問題我試過很多方法,比如速度跟蹤類(VelocityTracker)計算速度,當速度大了再根據滑動方向直接置頂或者置底,獲取廣告item可見性置頂或者置底.....等等。有些方法可能有點用,但是太麻煩了,最後直接在for迴圈中用firstPositionlastPosition,這樣,雖然會出現rate = - 0.2 這樣的負值,但是你只要給個判斷就可以了:

if (rate <= 0){
   rate = 0;
}
if (rate >= 1) {
   rate = 1;
}
複製程式碼

剛已經通過recycleView的監聽得到了廣告item位置 與 廣告有效移動距離,而 圖片有效移動距離呢,它在自定義imageView中的onDraw方法得到:

int maxDy = h - itemHeight;//圖片可以移動的最大距離為(圖片有效移動距離): (0 ~ -maxDy)

最後,呼叫canvas.translate(0, -rate * maxDy);方法就可以實現整個效果了。


對於生活理想,應該像宗教徒對待宗教一樣充滿虔誠與熱情!

相關文章