View的繪制(5)-小紅書(shū)歡迎頁(yè)的視差效果實(shí)現(xiàn)

主目錄見(jiàn):Android高級(jí)進(jìn)階知識(shí)(這是總目錄索引)

一.目標(biāo)

看了上一篇《換膚框架(一)之Support v7庫(kù)解析》想必大家很期待換膚框架(二),但是為什么冒死把這一篇提前呢某弦?這里有個(gè)原因就是為了給大家鞏固下support v7里面的知識(shí)點(diǎn)刀崖,以便到時(shí)能更容易理解換膚框架亮钦,如果你現(xiàn)在去看我等你十分鐘蜂莉。映穗。蚁滋。

正經(jīng)的程序員

大家想要源代碼可以重?fù)粝螺d睦霎,當(dāng)然這也不是原創(chuàng)代碼了,但是這個(gè)地方可以用來(lái)說(shuō)明這個(gè)知識(shí)點(diǎn)副女。

效果圖

二.代碼分析

1.基礎(chǔ)用法

這個(gè)地方用法很簡(jiǎn)單碑幅,直接看下面使用的代碼:
1.1)第一步:在布局里面添加

 <com.lenovohit.redbookparallx.ParallxContainer
        android:id="@+id/parallax_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

1.2)第二步:給這個(gè)ParallxContainer 設(shè)置頁(yè)面

     ParallxContainer container = (ParallxContainer) findViewById(R.id.parallax_container);
        container.setUp(
                new int[]{
                        R.layout.view_intro_1,
                        R.layout.view_intro_2,
                        R.layout.view_intro_3,
                        R.layout.view_intro_4,
                        R.layout.view_intro_5,
                        R.layout.view_login
                }
        );

        //設(shè)置動(dòng)畫(huà)
        ImageView iv_man = (ImageView) findViewById(R.id.iv_man);
        iv_man.setBackgroundResource(R.drawable.man_run);
        container.setIv_man(iv_man);

但是這個(gè)地方布局里面會(huì)有自定義的屬性,舉一個(gè)頁(yè)面view_intro_1說(shuō)明一下:

 <ImageView
        android:id="@+id/iv_0"
        android:layout_width="103dp"
        android:layout_height="19dp"
        android:layout_centerInParent="true"
        android:src="@mipmap/intro1_item_0"
        app:x_in="1.2"
        app:x_out="1.2" />

大家看下第一個(gè)頁(yè)面里面的一個(gè)ImageView添加了 app:x_in拷窜, app:x_out涧黄,這里ImageView原本是沒(méi)有這個(gè)屬性的笋妥,那么我們要怎么去識(shí)別這些屬性并且能設(shè)置給ImageView呢春宣?答案就是我們會(huì)自定義一個(gè)繼承LayoutInflater的類(lèi)去攔截View的創(chuàng)建過(guò)程。如果不知道這個(gè)知識(shí)點(diǎn)的可以倒帶到《換膚框架(一)之Support v7庫(kù)解析》里面講的非常清楚嚷辅。

2.自定義LayoutInflater

自定義LayoutInflater的話要去繼承LayoutInflater簸搞,然后會(huì)強(qiáng)制你實(shí)現(xiàn)他的構(gòu)造方法和cloneInContext(Context context)方法趁俊。
?1.首先我們看下cloneInContext(Context context)方法:

  @Override
    public LayoutInflater cloneInContext(Context context) {
        return new ParallaxLayoutInflater(this,context,fragment);
    }

我們看到這個(gè)方法就是返回一個(gè)LayoutInflater 對(duì)象寺擂,所以我們把我們的自定義LayoutInflater對(duì)象返回即可般卑。
?2.然后我們看下構(gòu)造方法:

   protected ParallaxLayoutInflater(LayoutInflater original, Context newContext,ParallaxFragment fragment) {
        super(original, newContext);
        this.fragment = fragment;
        //重新設(shè)置布局加載器的工廠
        //工廠:創(chuàng)建布局文件中所有的視圖
        LayoutInflaterCompat.setFactory(this, new ParallaxFactory(this));
    }

我們看到這里有一句關(guān)鍵代碼蝠检,就是調(diào)用LayoutInflaterCompat的setFactory方法叹谁,其實(shí)在LayoutInflater中就有setFactory方法,為什么這個(gè)地方要調(diào)用LayoutInflaterCompat的setFactory方法呢析苫?答案是兼容性衩侥,這個(gè)類(lèi)是兼容包support里面的,不然在繼承了AppCompatActivity之后會(huì)沒(méi)有效果矛物。
這個(gè)地方重新提下為什么要設(shè)置Factory峦萎,原因很簡(jiǎn)單就是setContentView()源碼分析里面有句話:

 if (mFactory2 != null) {
                view = mFactory2.onCreateView(parent, name, context, attrs);
            } else if (mFactory != null) {
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }

可以看出只要我們?cè)O(shè)置了我們自定義的Factory然后重寫(xiě)onCreateView方法就可以攔截View的創(chuàng)建過(guò)程了爱榔,這樣就可以攔截到前面說(shuō)的ImageView沒(méi)有的 app:x_in详幽, app:x_out屬性悴能,達(dá)到設(shè)置給ImageView的目的漠酿。

3.自定義factory

前面我們看到我們要設(shè)置一個(gè)自己的Factory炒嘲,那么這個(gè)Factory怎么自定義呢?首先第一步要繼承LayoutInflaterFactory,然后重寫(xiě)onCreateView方法夭拌,這也是前面說(shuō)過(guò)的鸽扁。方法如下:

   @Override
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
            View view = null;
            if (view == null) {
                view = createViewOrFailQuietly(name,context,attrs);
            }
            //實(shí)例化完成
            if (view != null) {
                //獲取自定義屬性,通過(guò)標(biāo)簽關(guān)聯(lián)到視圖上
                setViewTag(view,context,attrs);
                fragment.getViews().add(view);
            }

            return view;
        }

我們看到createViewOrFailQuietly這一句笆呆,這個(gè)方法就是創(chuàng)建一個(gè)View,方法是我們自己實(shí)現(xiàn)的东囚,方法如下:

  private View createViewOrFailQuietly(String name, Context context,
                                             AttributeSet attrs) {
            //1.自定義控件標(biāo)簽名稱(chēng)帶點(diǎn),所以創(chuàng)建時(shí)不需要前綴
            if (name.contains(".")) {
                createViewOrFailQuietly(name, null, context, attrs);
            }
            //2.系統(tǒng)視圖需要加上前綴
            for (String prefix : sClassPrefix) {
                View view = createViewOrFailQuietly(name, prefix, context, attrs);
                if (view != null) {
                    return view;
                }
            }
            return null;
        }

方法的意思就是
1.如果是自己自定義的控件的話就不用加上前綴桨嫁,為什么呢植兰?原因是因?yàn)槲覀冏远x控件寫(xiě)進(jìn)xml文件的時(shí)候是包名+類(lèi)名例如com.lenovohit.redbookparallx.ParallxContainer,這樣在系統(tǒng)反射創(chuàng)建這個(gè)類(lèi)的時(shí)候是可以成功的璃吧。
2.而如果是系統(tǒng)控件如ImageVIew是沒(méi)有全稱(chēng)的楣导,所以我們需要加上前綴,就是包名畜挨,這個(gè)我們得看我們用到的幾個(gè)控件分別在哪幾個(gè)包里面然后放進(jìn)sClassPrefix數(shù)組里筒繁,最后分別遍歷去嘗試創(chuàng)建噩凹。
具體的創(chuàng)建代碼如下:

   private View createViewOrFailQuietly(String name, String prefix, Context context,
                                             AttributeSet attrs) {
            try {
                //通過(guò)系統(tǒng)的inflater創(chuàng)建視圖,讀取系統(tǒng)的屬性
                return inflater.createView(name, prefix, attrs);
            } catch (Exception e) {
                return null;
            }
        }

這個(gè)地方的inflater就是我們自己自定義的LayoutInflater毡咏,當(dāng)然最后創(chuàng)建完視圖,我們需要將我們自定義的屬性和視圖View關(guān)聯(lián)起來(lái),以便于我們?cè)诨瑒?dòng)的時(shí)候可以取出來(lái)設(shè)置。
所以我們最后調(diào)用了:

      //實(shí)例化完成
            if (view != null) {
                //獲取自定義屬性,通過(guò)標(biāo)簽關(guān)聯(lián)到視圖上
                setViewTag(view,context,attrs);
                fragment.getViews().add(view);
            }

這里面的setViewTag就是取出自定義屬性娄涩,然后將它設(shè)置給View:

  private void setViewTag(View view, Context context, AttributeSet attrs) {
            //所有自定義的屬性
            int[] attrIds = {
                    R.attr.a_in,
                    R.attr.a_out,
                    R.attr.x_in,
                    R.attr.x_out,
                    R.attr.y_in,
                    R.attr.y_out};

            //獲取
            TypedArray a = context.obtainStyledAttributes(attrs, attrIds);
            if (a != null && a.length() > 0) {
                //獲取自定義屬性的值
                ParallaxViewTag tag = new ParallaxViewTag();
                tag.alphaIn = a.getFloat(0, 0f);
                tag.alphaOut = a.getFloat(1, 0f);
                tag.xIn = a.getFloat(2, 0f);
                tag.xOut = a.getFloat(3, 0f);
                tag.yIn = a.getFloat(4, 0f);
                tag.yOut = a.getFloat(5, 0f);

                //index
                view.setTag(R.id.parallax_view_tag,tag);
            }

            a.recycle();
        }

到這里我們的自定義LayoutInflater已經(jīng)自定義完畢。其實(shí)這就是這篇文章的關(guān)鍵點(diǎn)张惹,為了講解代碼的完整性,這里還不算完,但是到這里我的目的已經(jīng)達(dá)到,先休息五分鐘唠帝。瀑晒。把介。

嗨起來(lái)

4.Fragment onCreateView

我們自定義完我們的LayoutInflater巢墅,那我們要使用呀,這篇文章的效果因?yàn)槲覀兪怯肰iewPager里面放置Fragment会前,所以我們的一頁(yè)一頁(yè)布局都是在Fragment里面反璃,所以我們?cè)贔ragment的onCreateView中創(chuàng)建視圖的時(shí)候用上它:

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        Bundle bundle = getArguments();
        int layoutId = bundle.getInt("layoutId");
        ParallaxLayoutInflater layoutInflater = new ParallaxLayoutInflater(inflater, getActivity(),this);
        return layoutInflater.inflate(layoutId, null);
    }

我們自定義的ParallaxLayoutInflater 派上用場(chǎng)了侧蘸,這個(gè)地方我們?cè)趧?chuàng)建視圖的時(shí)候就會(huì)走我們自己的LayoutInflater了逢艘,內(nèi)心是不是無(wú)比激動(dòng)。

5.ParallxContainer setUp

我們開(kāi)頭在基礎(chǔ)用法里面看到我們用的時(shí)候會(huì)調(diào)用這個(gè)方法崩瓤,那么我們來(lái)看這個(gè)方法做了啥:

    public void setUp(int... childIds) {
        fragments = new ArrayList<>();
        for (int i = 0; i < childIds.length; i++) {
            ParallaxFragment f = new ParallaxFragment();
            Bundle args = new Bundle();
            //頁(yè)面索引
            args.putInt("index", i);
            //Fragment中需要加載的布局文件id
            args.putInt("layoutId", childIds[i]);
            f.setArguments(args);
            fragments.add(f);
        }

        //實(shí)例化適配器
        SplashActivity activity = (SplashActivity)getContext();
        adapter = new ParallaxAdapter(activity.getSupportFragmentManager(), fragments);

        //實(shí)例化ViewPager
        ViewPager vp = new ViewPager(getContext());
        vp.setId(R.id.parallax_pager);
        vp.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT));

        //綁定
        vp.setAdapter(adapter);
        addView(vp,0);

        vp.addOnPageChangeListener(this);
    }

到現(xiàn)在大家應(yīng)該都看得懂這段代碼黔攒,就是創(chuàng)建出幾個(gè)Fragment,然后實(shí)例化適配器給ViewPager旅掂,最后設(shè)置了個(gè)ViewPager的監(jiān)聽(tīng)。這個(gè)監(jiān)聽(tīng)還蠻重要的栅哀,因?yàn)槲覀兘壎薞iew和自定義的屬性之后疫向,在這邊要取出來(lái)真正根據(jù)自定義的屬性來(lái)設(shè)置動(dòng)畫(huà)呀。

6.ParallxContainer onPageScrolled

我們的ParallxContainer 實(shí)現(xiàn)了OnPageChangeListener接口琢蛤,所以這幾個(gè)方法也在這個(gè)類(lèi)里面重寫(xiě)了儡率。第一個(gè)是滑動(dòng)時(shí)候要做的事情:

    @Override
    public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
        this.containerWidth = getWidth();
        //在翻頁(yè)的過(guò)程中挂据,不斷根據(jù)視圖的標(biāo)簽中對(duì)應(yīng)的動(dòng)畫(huà)參數(shù),改變視圖的位置或者透明度
        //獲取到進(jìn)入的頁(yè)面
        ParallaxFragment inFragment = null;
        try {
            inFragment = fragments.get(position - 1);
        } catch (Exception e) {}

        //獲取到退出的頁(yè)面
        ParallaxFragment outFragment = null;
        try {
            outFragment = fragments.get(position);
        } catch (Exception e) {}

        if (inFragment != null) {
            //獲取Fragment上所有的視圖儿普,實(shí)現(xiàn)動(dòng)畫(huà)效果
            List<View> inViews = inFragment.getViews();
            if (inViews != null) {
                for (View view : inViews) {
                    //獲取標(biāo)簽崎逃,從標(biāo)簽上獲取所有的動(dòng)畫(huà)參數(shù)
                    ParallaxViewTag tag = (ParallaxViewTag) view.getTag(R.id.parallax_view_tag);
                    if (tag == null) {
                        continue;
                    }
                    //translationY改變view的偏移位置,translationY=100眉孩,代表view在其原始位置向下移動(dòng)100
                    //仔細(xì)觀察進(jìn)入的fragment中view從遠(yuǎn)處過(guò)來(lái)个绍,不斷向下移動(dòng),最終停在原始位置
                    ViewHelper.setTranslationY(view, (containerWidth - positionOffsetPixels) * tag.yIn);
                    ViewHelper.setTranslationX(view, (containerWidth - positionOffsetPixels) * tag.xIn);
                }
            }
        }

        if(outFragment != null){
            List<View> outViews = outFragment.getViews();
            if (outViews != null) {
                for (View view : outViews) {
                    ParallaxViewTag tag = (ParallaxViewTag) view.getTag(R.id.parallax_view_tag);
                    if (tag == null) {
                        continue;
                    }
                    //仔細(xì)觀察退出的fragment中view從原始位置開(kāi)始向上移動(dòng)浪汪,translationY應(yīng)為負(fù)數(shù)
                    ViewHelper.setTranslationY(view, 0 - positionOffsetPixels * tag.yOut);
                    ViewHelper.setTranslationX(view, 0 - positionOffsetPixels * tag.xOut);
                }
            }
        }
    }

這段代碼注釋還是比較清楚的巴柿,我在這里簡(jiǎn)單說(shuō)一下邏輯,這個(gè)地方就是取出前一個(gè)fragment和后一個(gè)要進(jìn)入的fragment死遭,然后遍歷這個(gè)fragment里面的視圖控件广恢,根據(jù)剛才視圖控件綁定的屬性來(lái)設(shè)置該控件的動(dòng)畫(huà)。

7.ParallxContainer onPageSelected

這個(gè)方法很簡(jiǎn)單呀潭,就是看目前處于第幾個(gè)頁(yè)面來(lái)判斷中間走路的女生要不要顯示:

   @Override
    public void onPageSelected(int position) {
        if (position == adapter.getCount() - 1) {
            iv_man.setVisibility(INVISIBLE);
        }else{
            iv_man.setVisibility(VISIBLE);
        }
    }

8.ParallxContainer onPageScrollStateChanged

下面這個(gè)方法也很簡(jiǎn)單钉迷,就是判斷ViewPager處于什么狀態(tài),跟著這個(gè)女生要開(kāi)始走動(dòng)還是不走動(dòng)钠署,這個(gè)地方的動(dòng)畫(huà)效果是用幀動(dòng)畫(huà)來(lái)做的糠聪。

 @Override
    public void onPageScrollStateChanged(int state) {
        AnimationDrawable animation = (AnimationDrawable) iv_man.getBackground();
        switch (state) {
            case ViewPager.SCROLL_STATE_DRAGGING:
                animation.start();
                break;

            case ViewPager.SCROLL_STATE_IDLE:
                animation.stop();
                break;

            default:
                break;
        }
    }

到這里我們就說(shuō)完我們的代碼了,其實(shí)除了那個(gè)知識(shí)點(diǎn)其他都是基礎(chǔ)谐鼎,想必不會(huì)難倒大家舰蟆,如果哪里有說(shuō)的不清楚的,大家可以留言說(shuō)一下该面。Have a good journey夭苗!
總結(jié):在這里依舊要總結(jié)一下,這個(gè)地方主要用到的知識(shí)點(diǎn)其實(shí)跟support v7庫(kù)用到的是一模一樣隔缀,所以很多知識(shí)是關(guān)聯(lián)的题造,希望大家循序漸進(jìn),不驕不躁猾瘸,在Android的旅程中快樂(lè)界赔,一起成長(zhǎng)丢习。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市淮悼,隨后出現(xiàn)的幾起案子咐低,更是在濱河造成了極大的恐慌,老刑警劉巖袜腥,帶你破解...
    沈念sama閱讀 222,000評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件见擦,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡羹令,警方通過(guò)查閱死者的電腦和手機(jī)鲤屡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)福侈,“玉大人酒来,你說(shuō)我怎么就攤上這事》玖荩” “怎么了堰汉?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,561評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)伟墙。 經(jīng)常有香客問(wèn)我翘鸭,道長(zhǎng),這世上最難降的妖魔是什么远荠? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,782評(píng)論 1 298
  • 正文 為了忘掉前任矮固,我火速辦了婚禮,結(jié)果婚禮上譬淳,老公的妹妹穿的比我還像新娘档址。我一直安慰自己,他們只是感情好邻梆,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,798評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布守伸。 她就那樣靜靜地躺著,像睡著了一般浦妄。 火紅的嫁衣襯著肌膚如雪尼摹。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,394評(píng)論 1 310
  • 那天剂娄,我揣著相機(jī)與錄音蠢涝,去河邊找鬼。 笑死阅懦,一個(gè)胖子當(dāng)著我的面吹牛和二,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播耳胎,決...
    沈念sama閱讀 40,952評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼惯吕,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼惕它!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起废登,我...
    開(kāi)封第一講書(shū)人閱讀 39,852評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤淹魄,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后堡距,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體甲锡,經(jīng)...
    沈念sama閱讀 46,409評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,483評(píng)論 3 341
  • 正文 我和宋清朗相戀三年吏颖,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了搔体。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,615評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡半醉,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出劝术,到底是詐尸還是另有隱情缩多,我是刑警寧澤,帶...
    沈念sama閱讀 36,303評(píng)論 5 350
  • 正文 年R本政府宣布养晋,位于F島的核電站衬吆,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏绳泉。R本人自食惡果不足惜逊抡,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,979評(píng)論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望零酪。 院中可真熱鬧冒嫡,春花似錦、人聲如沸四苇。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,470評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)月腋。三九已至蟀架,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間榆骚,已是汗流浹背片拍。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,571評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留妓肢,地道東北人捌省。 一個(gè)月前我還...
    沈念sama閱讀 49,041評(píng)論 3 377
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像职恳,于是被迫代替她去往敵國(guó)和親所禀。 傳聞我的和親對(duì)象是個(gè)殘疾皇子方面,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,630評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容