温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

如何Android项目中创建一个View

发布时间:2020-11-25 16:25:50 来源:亿速云 阅读:207 作者:Leah 栏目:移动开发

这篇文章将为大家详细讲解有关如何Android项目中创建一个View,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。

从布局文件到LayoutParams

首先从Activity的setContentView(int)方法开始,只要设置了R.layout的布局文件,那么界面上就会显示出来对应的内容。所以以这个方法为初发点,然后往后跟踪代码。

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

通过以上代码发现调用了Window类的setContentView方法,那么这个Window对象mWindow又是怎么初始化的?在Activity中搜索发现是在Activity的attach方法中初始化的,构造了一个PhoneWindow对象。

如下代码所示:

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); // 这里创建了Window对象
 mWindow.setCallback(this);
 mWindow.setOnWindowDismissedCallback(this);
 mWindow.getLayoutInflater().setPrivateFactory(this);
 // ... 中间部分代码省略
 mWindow.setWindowManager(
   (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
   mToken, mComponent.flattenToString(),
   (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
 if (mParent != null) {
  mWindow.setContainer(mParent.getWindow());
 }
 mWindowManager = mWindow.getWindowManager();
 mCurrentConfig = config;
}

在PhoneWindow的setContentView(int)方法中,发现是调用了LayoutInflater的inflate(int, View)方法,对这个布局文件进行转换成View,并添加到后面View这个参数中。

@Override
public void setContentView(int layoutResID) {
 // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
 // decor, when theme attributes and the like are crystalized. Do not check the feature
 // before this happens.
 if (mContentParent == null) {
  installDecor();
 } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
  mContentParent.removeAllViews();
 }
 if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
  // ...
 } else {
  mLayoutInflater.inflate(layoutResID, mContentParent);
 }
 // ...
}

这里面顺带穿插说一下View的根节点是怎样初始化出来的。

这里有一个关键的地方是这个installDecor()方法,在这个方法中通过调用generateDecor()方法创建了这个mDecor的对象,通过调用generateLayout(DecorView)方法初始化出来了mContentParent对象。其中,mDecor是PhoneWindow.DecorView的类实例,mContentParent是展示所有内容的,是通过com.android.internal.R.id.contentID来找到这个View。

具体代码如下所示:

protected ViewGroup generateLayout(DecorView decor) {
 // ...
 View in = mLayoutInflater.inflate(layoutResource, null); // layoutResource是根据对当前显示View的Activity的theme属性值来决定由系统加载对应的布局文件
 decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
 mContentRoot = (ViewGroup) in;
 
 ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
 if (contentParent == null) {
 throw new RuntimeException("Window couldn't find content container view");
 }
 // ...
 return contentParent;
}

那么在哪里面可以看到这个DecorView呢?看下面图。

下面这张图是在debug模式下连接手机调试App,使用Layout Inspector工具查看得到的图:

如何Android项目中创建一个View
PhoneWindow.DecorView

其中从1位置可以看出,整个View的根节点的View是PhoneWindow.DecorView实例;从2位置和3位置的mId可以推断出来,上面的mContentParent就是ContentFrameLayout类的实例;位置4中的蓝色区域是mContentParent所表示的位置和大小。

以上图是在AS 2.2.3版本上使用Android Monitor Tab页中的Layout Inspector工具(参考位置5)生成。

紧接着跟踪上面LayoutInflater中的inflate()方法中调用,发现最后调用到了
inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)方法中,在这个方法中构造了XmlResourceParser对象,而这个parser对象构造代码如下所示:

XmlResourceParser loadXmlResourceParser(int id, String type)
  throws NotFoundException {
 synchronized (mAccessLock) {
  TypedValue value = mTmpValue;
  if (value == null) {
   mTmpValue = value = new TypedValue();
  }
  getValue(id, value, true);
  if (value.type == TypedValue.TYPE_STRING) {
   return loadXmlResourceParser(value.string.toString(), id,
     value.assetCookie, type);
  }
  throw new NotFoundException(
    "Resource ID #0x" + Integer.toHexString(id) + " type #0x"
    + Integer.toHexString(value.type) + " is not valid");
 }
}
XmlResourceParser loadXmlResourceParser(String file, int id,
  int assetCookie, String type) throws NotFoundException {
 if (id != 0) {
  // ...
  // These may be compiled...
  synchronized (mCachedXmlBlockIds) {
   // First see if this block is in our cache.
   final int num = mCachedXmlBlockIds.length;
   for (int i=0; i<num; i++) {
    if (mCachedXmlBlockIds[i] == id) {
     //System.out.println("**** REUSING XML BLOCK! id="
     //     + id + ", index=" + i);
     return mCachedXmlBlocks[i].newParser();
    }
   }
   // Not in the cache, create a new block and put it at
   // the next slot in the cache.
   XmlBlock block = mAssets.openXmlBlockAsset(
     assetCookie, file);
   if (block != null) {
    int pos = mLastCachedXmlBlockIndex+1;
    if (pos >= num) pos = 0;
    mLastCachedXmlBlockIndex = pos;
    XmlBlock oldBlock = mCachedXmlBlocks[pos];
    if (oldBlock != null) {
     oldBlock.close();
    }
    mCachedXmlBlockIds[pos] = id;
    mCachedXmlBlocks[pos] = block;
    //System.out.println("**** CACHING NEW XML BLOCK! id="
    //     + id + ", index=" + pos);
    return block.newParser();
   }
  }
  // ...
 }
 // ...
}

其中getValue()方法调用到本地frameworks/base/core/jni/android_util_AssetManager.cpp文件中的static jint android_content_AssetManager_loadResourceValue函数,而在这个函数中java层的TypeValue的类对象value对象属性通过JNI形式被赋值。

在构建这个parser时,经历了很复杂的过程,从TypeValue中的assetCookie属性,到XmlBlock中的xmlBlock属性,再到XmlBlock.Parser中的mParseState属性,都是用来保存JNI层的数据,因为最终操作这些资源数据是在native层,所以必不可少通过JNI这种方式,在java层和native层周旋。这里没有深入到native层去分析资源是如何被加载解析的,后面有机会再说。

最后跟到了这个public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)方法中,代码如下所示:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
 synchronized (mConstructorArgs) {
 final Context inflaterContext = mContext;
 final AttributeSet attrs = Xml.asAttributeSet(parser);
 Context lastContext = (Context) mConstructorArgs[0];
 View result = root;
 // ...
 
 // Temp is the root view that was found in the xml
 final View temp = createViewFromTag(root, name, inflaterContext, attrs); // 这里将布局文件中的名称反射成具体的View类对象
 ViewGroup.LayoutParams params = null;
 if (root != null) {
  // Create layout params that match root, if supplied
  params = root.generateLayoutParams(attrs); // 这里将尺寸转换成了LayoutParams
  if (!attachToRoot) {
   // Set the layout params for temp if we are not
   // attaching. (If we are, we use addView, below)
   temp.setLayoutParams(params);
  }
 }
 // Inflate all children under temp against its context.
 rInflateChildren(parser, temp, attrs, true);
 // 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); // 将布局文件中根的View添加到mContentParent中
 }
  
 // ...
 return result;
}

接着看View的generateLayoutParams(AttributeSet)方法,因为这里返回了params。查看代码最后发现LayoutParams的width和height属性赋值的代码如下所示:

public LayoutParams(Context c, AttributeSet attrs) {
 TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
 setBaseAttributes(a,
   R.styleable.ViewGroup_Layout_layout_width,
   R.styleable.ViewGroup_Layout_layout_height);
 a.recycle();
}
 
protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
 width = a.getLayoutDimension(widthAttr, "layout_width");
 height = a.getLayoutDimension(heightAttr, "layout_height");
}

通过查看TypedArray类中的getLayoutDimension()方法发现,获取的值是通过index在mData这个成员数组中获取的。这个mData的值是在创建TypedArray对象时被赋的值,具体参见TypedArray的obtain方法。这个数组是在Resources的obtainStyledAttributes()方法中通过调用AssetManager.applyStyle()方法被初始化值的。applyStyle()方法是一个native方法,对应frameworks/base/core/jni/android_util_AssetManager.cpp文件中的android_content_AssetManager_applyStyle函数。在这个函数中发现,传入的Resrouces类中的mTheme成员以及XmlBlock.Parse类的mParseState成员都是一个C++对象的指针,在java类中以整型或长整型值保存。

至此,布局文件中的尺寸值已经被转换成了具体的int类型值。

从布局文件到View

从上面的public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)方法中看到View是通过这行代码final View temp = createViewFromTag(root, name, inflaterContext, attrs);创建出来的,而这个name就是XML文件在解析时遇到的标签名称,比如TextView。此时的attrs也就是上面分析的XmlBlock.Parser的对象。最后发现View是在createViewFromTag()方法中创建的,代码如下所示:

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
  boolean ignoreThemeAttr) {
 if (name.equals("view")) {
  name = attrs.getAttributeValue(null, "class");
 }
 
 // Apply a theme wrapper, if allowed and one is specified.
 if (!ignoreThemeAttr) {
  final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
  final int themeResId = ta.getResourceId(0, 0);
  if (themeResId != 0) {
   context = new ContextThemeWrapper(context, themeResId);
  }
  ta.recycle();
 }
 // ...
 View view;
 if (mFactory2 != null) {
  view = mFactory2.onCreateView(parent, name, context, attrs);
 } else if (mFactory != null) {
  view = mFactory.onCreateView(name, context, attrs);
 } else {
  view = null;
 }
 if (view == null && mPrivateFactory != null) {
  view = mPrivateFactory.onCreateView(parent, name, context, attrs);
 }
 if (view == null) {
  final Object lastContext = mConstructorArgs[0];
  mConstructorArgs[0] = context;
  try {
   if (-1 == name.indexOf('.')) {
    view = onCreateView(parent, name, attrs);
   } else {
    view = createView(name, null, attrs);
   }
  } finally {
   mConstructorArgs[0] = lastContext;
  }
 }
 return view;
 
 // ...
}

这里要注意一下,mConstructorArgs的第一个值是一个Context,而这个Context有可能已经不是当前Activity的Context。

看到这里,下面这样的代码片段是不是很熟悉?

if (name.equals("view")) {
 name = attrs.getAttributeValue(null, "class");
}

上面Factory这个接口对象在LayoutInflater类中就有三个属性,分别对应:factory、factory2、mPrivateFactory。很明显,弄清了这三个对象,也就知道了View的初始化流程。下面代码是对这三个属性的值的输出:

public class MainActivity extends AppCompatActivity {
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  LayoutInflater inflater = getLayoutInflater();
  LayoutInflater inflater1 = LayoutInflater.from(this);
  Field f = null;
  try {
   f = LayoutInflater.class.getDeclaredField("mPrivateFactory");
   f.setAccessible(true);
  } catch (NoSuchFieldException e) {
   e.printStackTrace();
  }
 Log.d("may", "the same object: " + (inflater == inflater1));
  Log.d("may", "inflater factory: " + inflater.getFactory() + ", factory2: " + inflater.getFactory2());
  Log.d("may", "inflater1 factory: " + inflater1.getFactory() + ", factory2: " + inflater1.getFactory2());
  if (f != null) {
   try {
    Log.d("may", "inflater mPrivateFactory: " + f.get(inflater));
    Log.d("may", "inflater1 mPrivateFactory: " + f.get(inflater1));
   } catch (IllegalAccessException e) {
    e.printStackTrace();
   }
  }
 }
}

输出的LOG如下所示:

// 当前Activiy继承的是android.support.v7.app.AppCompatActivity
the same object: true
inflater factory: android.support.v4.view.LayoutInflaterCompatHC$FactoryWrapperHC{android.support.v7.app.AppCompatDelegateImplV14@41fdf0b0}, factory2: android.support.v4.view.LayoutInflaterCompatHC$FactoryWrapperHC{android.support.v7.app.AppCompatDelegateImplV14@41fdf0b0}
inflater1 factory: android.support.v4.view.LayoutInflaterCompatHC$FactoryWrapperHC{android.support.v7.app.AppCompatDelegateImplV14@41fdf0b0}, factory2: android.support.v4.view.LayoutInflaterCompatHC$FactoryWrapperHC{android.support.v7.app.AppCompatDelegateImplV14@41fdf0b0}
inflater mPrivateFactory: com.jacpy.sb.MainActivity@41fd9e70
inflater1 mPrivateFactory: com.jacpy.sb.MainActivity@41fd9e70
// 当前Activity继承的是android.app.Activity
the same object: true
inflater factory: null, factory2: null
inflater1 factory: null, factory2: null
inflater mPrivateFactory: com.jacpy.sb.MainActivity@41fd9a28
inflater1 mPrivateFactory: com.jacpy.sb.MainActivity@41fd9a28

首先看到mPrivateFactory是当前的Activity实例,因为Activity也实现的Factory2接口。首先看LayoutInflater的创建过程,如下图所示:

如何Android项目中创建一个ViewLayoutInflater初始化流程

而生成的PhoneLayoutInflater对象是缓存在ContextImpl类的属性SYSTEM_SERVICE_MAP中,所以通过Context.LAYOUT_INFLATER_SERVIC去取,始终是同一个对象,当然仅限于当前Context中。

mPrivateFactory属性的赋值是在Activity的attach()方法中,通过调用mWindow.getLayoutInflater().setPrivateFactory(this); ,因此调用Factory2的onCreateView()方法时,实际是调用Activity中的onCreateView()方法。而Activity中的onCreateView()实际返回的是null,所以最后创建View的是if判断中的onCreateView(parent, name, attrs)方法,最后View是在LayoutInflater类中的public final View createView(String name, String prefix, AttributeSet attrs) throws ClassNotFoundException, InflateException方法中创建:

public final View createView(String name, String prefix, AttributeSet attrs)
  throws ClassNotFoundException, InflateException {
 Constructor<&#63; extends View> constructor = sConstructorMap.get(name);
 Class<&#63; extends View> clazz = null;
 // ...
 // Class not found in the cache, see if it's real, and try to add it
 clazz = mContext.getClassLoader().loadClass(
   prefix != null &#63; (prefix + name) : name).asSubclass(View.class); // prefix传的值是android.view.
 
 // ...
 
 constructor = clazz.getConstructor(mConstructorSignature); // Class<&#63;>[] mConstructorSignature = new Class[] {
   Context.class, AttributeSet.class};
 constructor.setAccessible(true);
 sConstructorMap.put(name, constructor);
 // ...
 Object[] args = mConstructorArgs;
 args[1] = attrs; // 这个值是XmlBlock.Parser对象
 final View view = constructor.newInstance(args);
 if (view instanceof ViewStub) {
  // Use the same context when inflating ViewStub later.
  final ViewStub viewStub = (ViewStub) view;
  viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
 }
 return view;
}

关于如何Android项目中创建一个View就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果觉得文章不错,可以把它分享出去让更多的人看到。

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI