android自定義view-視差動畫

慚愧 發現已經好久沒更新博客了,來一發
前面有寫過一篇自定義view 主要寫的是為原生的控件添加自定義的屬性,其基本原理就是在代碼中為原生的控件外面包一層自定義的控件,從而使系統能認識我們自定義的屬性,最終達到控制原生控件的目的。這樣做的目的是為了讓別人用我們設計的框架時,不需要為了一個屬性而去自定義view。
如果有興趣詳細了解可以參考我的這篇文章android 自定義ViewGroup之浪漫求婚

今天我們繼續來研究另外一種實現方式。這種方式是小紅書的歡迎頁面的實現方式

首先我們還是要來看一下activity加載布局的流程,因為不管哪種方式最終都是通過在加載布局的過程中,人為的控制加載的屬性,來達到我們簡化開發的目的。

 protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ...
 }

上面的代碼相信大家都看的懂就不解釋了,當調用setContentView方法時會去調用它的父類方法 Activity.java:

public void setContentView(int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

然后接著調用getWindow()的setContentView,所以我們首先要知道getWidow()是什么,依然在Activity.java中看到:

public Window getWindow() {
        return mWindow;
    }

所以最終是mWindow,那么mWindow是什么呢

private Window mWindow;

他是一個Window,而Window是一個抽象類,所以我們得回到Activity.java中找它的賦值語句
在attach方法中我們找到了mWindow的賦值

  final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
        attachBaseContext(context);

        mFragments.attachHost(null /*parent*/);

        mWindow = new PhoneWindow(this);
        ...
        }
        

發現它其實是PhoneWindow類,因此我們去到PhoneWindow類里面看看setContentView的具體實現:

 public void setContentView(int layoutResID) {
      ...
            mLayoutInflater.inflate(layoutResID, mContentParent);
       ...
    }

只看關鍵代碼 發現最后是通過LayoutInflater.inflate來加載布局的,大家應該都知道 findViewById 可以用來查找控件,inflate用來查找布局,所以發現系統其實也是這樣做的。
這里有個注意事項inflate的時候其實已經把布局給畫到視圖上了,曾經因為這個問題困擾了我一個同事好久。

而最終inflate 會調到LayoutInflater.java的inflate(int resource, ViewGroup root, boolean attachToRoot)

 public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                    + Integer.toHexString(resource) + ")");
        }

        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

到這里可以發現 首先會用XmlResourceParser 去解析我們設置進來的布局參數,然后返回inflate(parser, root, attachToRoot)
這里面的代碼就比較多了

 public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

                ...
                final String name = parser.getName();
                
                if (DEBUG) {
                    System.out.println("**************************");
                    System.out.println("Creating root view: "
                            + name);
                    System.out.println("**************************");
                }

                if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }

                    rInflate(parser, root, attrs, false, false);
                } else {
                    // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, attrs, false);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        if (DEBUG) {
                            System.out.println("Creating params from root: " +
                                    root);
                        }
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }

                    if (DEBUG) {
                        System.out.println("-----> start inflating children");
                    }
                    // Inflate all children under temp
                    rInflate(parser, temp, attrs, true, true);
                    if (DEBUG) {
                        System.out.println("-----> done inflating children");
                    }

                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
                InflateException ex = new InflateException(e.getMessage());
                ex.initCause(e);
                throw ex;
            } catch (IOException e) {
                InflateException ex = new InflateException(
                        parser.getPositionDescription()
                        + ": " + e.getMessage());
                ex.initCause(e);
                throw ex;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;
            }

            Trace.traceEnd(Trace.TRACE_TAG_VIEW);

            return result;
        }
    }

從上面代碼中可以看出 會調用createViewFromTag來創建一個view,而最終也可以發現整個方法最后返回的也是這個view。因此我們繼續看下這個view是怎么創建出來的:

 View createViewFromTag(View parent, String name, AttributeSet attrs, boolean inheritContext) {
       ...
        // Apply a theme wrapper, if requested.
        final TypedArray ta = viewContext.obtainStyledAttributes(attrs, ATTRS_THEME);
        final int themeResId = ta.getResourceId(0, 0);
        if (themeResId != 0) {
            viewContext = new ContextThemeWrapper(viewContext, themeResId);
        }
        ta.recycle();

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

            if (view == null && mPrivateFactory != null) {
                view = mPrivateFactory.onCreateView(parent, name, viewContext, attrs);
            }

            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = viewContext;
                try {
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(parent, name, attrs);
                    } else {
                        view = createView(name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

            if (DEBUG) System.out.println("Created view is: " + view);
            return view;

    ...
        }
    }

一些與主題無關的判斷就暫時去掉了 重點關注下主線這里的view創建流程,發現這里新建一個view對象,然后一步一步的判讀,到最后只有Factory 沒有創建出view實例時才會調用它自己createView去創建view實例。然后我們在看下
Factory是什么

public interface Factory {
        /**
         * Hook you can supply that is called when inflating from a LayoutInflater.
         * You can use this to customize the tag names available in your XML
         * layout files.
         * 
         * <p>
         * Note that it is good practice to prefix these custom names with your
         * package (i.e., com.coolcompany.apps) to avoid conflicts with system
         * names.
         * 
         * @param name Tag name to be inflated.
         * @param context The context the view is being created in.
         * @param attrs Inflation attributes as specified in XML file.
         * 
         * @return View Newly created view. Return null for the default
         *         behavior.
         */
        public View onCreateView(String name, Context context, AttributeSet attrs);
    }

它就是一個接口,而且只有一個方法,但是注釋卻有好多 其實一看Hook 大致就明白了,它是一個鉤子,Factory2繼承自Factory所以Factory2是Factory的進一步擴充,而它的onCreateView方法可以返回一個view,搜索發現,發現在Activity.java中有實現這個接口而onCreateView返回是null,所以最終這個view的創建默認還是調用LayoutInflater的createView(name, null, attrs);
現在想當我們實現Factory這個接口,是不是就可以控制系統的控件了呢。

按照這個想法 我們來看看小紅書的歡迎頁面是怎么做的。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="fill_parent"
                android:layout_height="fill_parent"
                android:background="@color/white"
                android:orientation="vertical" >

    <com.xhs.view.parallaxpager.ParallaxContainer
        android:id="@+id/parallax_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

    <ImageView
        android:id="@+id/iv_man"
        android:layout_width="67dp"
        android:layout_height="202dp"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="10dp"
        android:background="@drawable/intro_item_manrun_1"
        android:visibility="gone" />

</RelativeLayout>

在布局中加入ParallaxContainer 這個自定義控件,它的實現如下

public void setupChildren(LayoutInflater inflater, int... childIds) {
       Log.i("lly3","getChildCount ==" +getChildCount());
       if (getChildCount() > 0) {
           throw new RuntimeException("setupChildren should only be called once when ParallaxContainer is empty");
       }

       ParallaxLayoutInflater parallaxLayoutInflater = new ParallaxLayoutInflater(
               inflater, getContext());
       for (int childId : childIds) {
           View view = parallaxLayoutInflater.inflate(childId, this);
           viewlist.add(view);
       }

       pageCount = getChildCount();
       for (int i = 0; i < pageCount; i++) {
           View view = getChildAt(i);
           addParallaxView(view, i);
       }

       updateAdapterCount();

       viewPager = new ViewPager(getContext());
       viewPager.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT));
       viewPager.setId(R.id.parallax_pager);

       viewPager.setAdapter(adapter);
       attachOnPageChangeListener();
       addView(viewPager, 0);
   }

只列出了主要的方法,源碼稍后會給出

上面的方法里我們主要關心

ParallaxLayoutInflater parallaxLayoutInflater = new ParallaxLayoutInflater(
                inflater, getContext());
        Log.i("lly3","getChildCount == 1," +getChildCount());
        for (int childId : childIds) {
            View view = parallaxLayoutInflater.inflate(childId, this);
            viewlist.add(view);
        }

這里自定義了一個LayoutInflater 我們看下ParallaxLayoutInflater類:

public class ParallaxLayoutInflater extends LayoutInflater {

  protected ParallaxLayoutInflater(LayoutInflater original, Context newContext) {
    super(original, newContext);
    setUpLayoutFactory();
  }

  private void setUpLayoutFactory() {
    if (!(getFactory() instanceof ParallaxFactory)) {
      setFactory(new ParallaxFactory(this, getFactory()));
    }
  }

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

setFactory的時候用的是自己實現的Factory,從而需要實現onCreateView方法
既然都自己實現這個方法了 那豈不我們想加什么就加什么了。

以下給出小紅書的歡迎頁面,大家也可以到網上搜索找到。
源碼

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,963評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,348評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,083評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,706評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,442評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,802評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,795評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,983評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,542評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,287評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,486評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,030評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,710評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,116評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,412評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,224評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,462評論 2 378

推薦閱讀更多精彩內容