1. 程式人生 > >android自定義view --視差動畫

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方法
既然都自己實現這個方法了 那豈不我們想加什麼就加什麼了。

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