Android实现动态换肤-原理篇

news2025/7/15 4:37:37

学习是一个过程。

文章目录

      • Activity中LayoutInflater加载布局总体时序图
      • LayoutInflater源码讲解(api28)
      • LayoutInflater设置Factory2
    • 实现方式
      • LayoutInflater源码总结

Activity中LayoutInflater加载布局总体时序图

在这里插入图片描述

LayoutInflater源码讲解(api28)

  • onCreate加载布局,是不是都很熟悉。

       @Override
        protected void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_happy);
        }
    
  • AppCompatActivity的onCreate()方法,注意这不是Activity的onCreate方法。

        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            final AppCompatDelegate delegate = getDelegate();
            //注意此方法是做换肤的关键。
            delegate.installViewFactory();
            delegate.onCreate(savedInstanceState);
            super.onCreate(savedInstanceState);
        }
    
    
  • AppCompatDelegatelmpl的installViewFactory方法,AppCompatDelegatelmpl实现了Factory2接口。

        public void installViewFactory() {
            LayoutInflater layoutInflater = LayoutInflater.from(mContext);
            if (layoutInflater.getFactory() == null) {
                //AppCompatDelegatelmpl实现了Factory2接口
                LayoutInflaterCompat.setFactory2(layoutInflater, this);
            } else {
                if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
                    Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                            + " so we can not install AppCompat's");
                }
            }
        }
    
  • LayoutInflater中的setFactory2方法,此方法不允许重复设置值,如果设置值会产生异常,所以如果做动态换肤设置Factory2时,要放在super.onCreate()方法之前,防止异常退出。

        public void setFactory2(Factory2 factory) {
            //从这可以看出factory是不可以重复设置值的,如果重复设置会产生异常。
            if (mFactorySet) {
                throw new IllegalStateException("A factory has already been set on this LayoutInflater");
            }
            if (factory == null) {
                throw new NullPointerException("Given factory can not be null");
            }
            mFactorySet = true;
            //mFactory与mFactory2一块赋值,mFractory2是按照扩展的方法进行开发的。
            if (mFactory == null) {
                mFactory = mFactory2 = factory;
            } else {
                mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
            }
        }
    
  • 继续分析setContentView()方法,AppCompatActivity中的setContentView调用的是AppCompatDelegatelmpl的方法。

      @Override
        public void setContentView(@LayoutRes int layoutResID) {
            getDelegate().setContentView(layoutResID);
        }
    
    
  • AppCompatDelegatelmpl的setContentView方法,此方法主要是加载我们自定义的布局,将布局添加到容器中。

      public void setContentView(int resId) {
          //主要是初始化根布局,用来存放我们自定义的布局。
            ensureSubDecor();
          //存放我们自定义布局的View,此View的类型是FrameLayout
            ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
            contentParent.removeAllViews();
          //加载自定义布局,将布局添加到contentParent中。
            LayoutInflater.from(mContext).inflate(resId, contentParent);
            mAppCompatWindowCallback.getWrapped().onContentChanged();
        }
    
    
  • LayoutInflater的inflate方法,此方法中有以后插件化用到的关键代码,此处先留意一下,以后有机会再进行分享插件化相关的技术。

        public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
            //此处是做插件化的关键,activity自定义getResources()方法,用来生产插件对应的资源。
            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();
            }
        }
    
    
  • 继续分析inflate方法,其重要流程是创建根布局,然后创建

     public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
            synchronized (mConstructorArgs) {
                Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
    
                final Context inflaterContext = mContext;
                final AttributeSet attrs = Xml.asAttributeSet(parser);
                Context lastContext = (Context) mConstructorArgs[0];
                mConstructorArgs[0] = inflaterContext;
                View result = root;
    
                try {
                    // Look for the root node.
                    int type;
                    while ((type = parser.next()) != XmlPullParser.START_TAG &&
                            type != XmlPullParser.END_DOCUMENT) {
                        // Empty
                    }
    
                    if (type != XmlPullParser.START_TAG) {
                        throw new InflateException(parser.getPositionDescription()
                                + ": No start tag found!");
                    }
    
                    final String name = parser.getName();
    
                    if (DEBUG) {
                        System.out.println("**************************");
                        System.out.println("Creating root view: "
                                + name);
                        System.out.println("**************************");
                    }
    
                    //处理 merge 标签
                    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, inflaterContext, attrs, false);
                    } else {
                        // Temp is the root view that was found in the xml
                        //自定义View的根布局,就是自己写的布局的根布局。
                        final View temp = createViewFromTag(root, name, inflaterContext, attrs);
    
                        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 against its context.
                        //把自定义的xml所有除根布局之外的控件全部实例化然后添加进根布局
                        rInflateChildren(parser, temp, attrs, 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) {
                    final InflateException ie = new InflateException(e.getMessage(), e);
                    ie.setStackTrace(EMPTY_STACK_TRACE);
                    throw ie;
                } catch (Exception e) {
                    final InflateException ie = new InflateException(parser.getPositionDescription()
                            + ": " + e.getMessage(), e);
                    ie.setStackTrace(EMPTY_STACK_TRACE);
                    throw ie;
                } finally {
                    // Don't retain static reference on context.
                    mConstructorArgs[0] = lastContext;
                    mConstructorArgs[1] = null;
    
                    Trace.traceEnd(Trace.TRACE_TAG_VIEW);
                }
    
                return result;
            }
        }
    
  • LayoutInflater的createViewFromTag方法,注意这里有一个BlinkLayout的闪烁小彩蛋,用来闪烁布局。

     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();
            }
    
         ``//闪烁的菜单,是为了庆祝??? 1995年庆祝什么节日?
            if (name.equals(TAG_1995)) {
                // Let's party like it's 1995!
                return new BlinkLayout(context, attrs);
            }
    
            try {
                View view;
    
                //如果是AppCompatActivity在这初始化,这个可以自己创建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);
                }
    
                //如果是Activity在这初始化。
                if (view == null) {
                    final Object lastContext = mConstructorArgs[0];
                    mConstructorArgs[0] = context;
                    try {
                        //不带包路径的View,最终都会调用到createView的这个方法
                        if (-1 == name.indexOf('.')) {
                            //这个最终会调用createView(name,“android.view.”,attrs),携带android.view前缀。
                            view = onCreateView(parent, name, attrs);
                        } else {
                            view = createView(name, null, attrs);
                        }
                    } finally {
                        mConstructorArgs[0] = lastContext;
                    }
                }
    
                return view;
            } catch (InflateException e) {
                throw e;
    
            } catch (ClassNotFoundException e) {
                final InflateException ie = new InflateException(attrs.getPositionDescription()
                        + ": Error inflating class " + name, e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
    
            } catch (Exception e) {
                final InflateException ie = new InflateException(attrs.getPositionDescription()
                        + ": Error inflating class " + name, e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            }
        }
    
  • 我们跟踪一下Layoutlnflater的createView方法,

    //利用反射创建对象,为啥不直接new对象呢?因为有些不能访问到?
    public final View createView(String name, String prefix, AttributeSet attrs)
                throws ClassNotFoundException, InflateException {
            Constructor<? extends View> constructor = sConstructorMap.get(name);
        
        	//鉴别构造方法是否失效,主要为了鉴别类加载器。
            if (constructor != null && !verifyClassLoader(constructor)) {
                constructor = null;
                sConstructorMap.remove(name);
            }
            Class<? extends View> clazz = null;
    
            try {
                Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
    
                if (constructor == null) {
                    // Class not found in the cache, see if it's real, and try to add it
                    clazz = mContext.getClassLoader().loadClass(
                            prefix != null ? (prefix + name) : name).asSubclass(View.class);
    
                    if (mFilter != null && clazz != null) {
                        boolean allowed = mFilter.onLoadClass(clazz);
                        if (!allowed) {
                            failNotAllowed(name, prefix, attrs);
                        }
                    }
                    constructor = clazz.getConstructor(mConstructorSignature);
                    constructor.setAccessible(true);
                    //为了增加效率,增加了缓存。
                    sConstructorMap.put(name, constructor);
                } else {
                    // If we have a filter, apply it to cached constructor
                    if (mFilter != null) {
                        // Have we seen this name before?
                        Boolean allowedState = mFilterMap.get(name);
                        if (allowedState == null) {
                            // New class -- remember whether it is allowed
                            clazz = mContext.getClassLoader().loadClass(
                                    prefix != null ? (prefix + name) : name).asSubclass(View.class);
    
                            boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
                            mFilterMap.put(name, allowed);
                            if (!allowed) {
                                failNotAllowed(name, prefix, attrs);
                            }
                        } else if (allowedState.equals(Boolean.FALSE)) {
                            failNotAllowed(name, prefix, attrs);
                        }
                    }
                }
    
                Object lastContext = mConstructorArgs[0];
                
                if (mConstructorArgs[0] == null) {
                    // Fill in the context if not already within inflation.
                    mConstructorArgs[0] = mContext;
                }
                
                Object[] args = mConstructorArgs;
                args[1] = attrs;
    
                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]));
                }
                
                mConstructorArgs[0] = lastContext;
                
                return view;
    
            } catch (NoSuchMethodException e) {
                final InflateException ie = new InflateException(attrs.getPositionDescription()
                        + ": Error inflating class " + (prefix != null ? (prefix + name) : name), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
    
            } catch (ClassCastException e) {
                // If loaded class is not a View subclass
                final InflateException ie = new InflateException(attrs.getPositionDescription()
                        + ": Class is not a View " + (prefix != null ? (prefix + name) : name), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } catch (ClassNotFoundException e) {
                // If loadClass fails, we should propagate the exception.
                throw e;
            } catch (Exception e) {
                final InflateException ie = new InflateException(
                        attrs.getPositionDescription() + ": Error inflating class "
                                + (clazz == null ? "<unknown>" : clazz.getName()), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
        }
    
    
  • 继续分析mFactory2.onCreateView()的方法,其最终会调用到AppCompatDelegatelmpl的createView(),此函数主要对mAppCompatViewInflater进行初始化,然后调用其createView()方法。

        public View createView(View parent, final String name, @NonNull Context context,
                @NonNull AttributeSet attrs) {
            if (mAppCompatViewInflater == null) {
                TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
                String viewInflaterClassName =
                        a.getString(R.styleable.AppCompatTheme_viewInflaterClass);
                if ((viewInflaterClassName == null)
                        || AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {
                    // Either default class name or set explicitly to null. In both cases
                    // create the base inflater (no reflection)
                    mAppCompatViewInflater = new AppCompatViewInflater();
                } else {
                    try {
                        Class<?> viewInflaterClass = Class.forName(viewInflaterClassName);
                        mAppCompatViewInflater =
                                (AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor()
                                        .newInstance();
                    } catch (Throwable t) {
                        Log.i(TAG, "Failed to instantiate custom view inflater "
                                + viewInflaterClassName + ". Falling back to default.", t);
                        mAppCompatViewInflater = new AppCompatViewInflater();
                    }
                }
            }
    
            boolean inheritContext = false;
            if (IS_PRE_LOLLIPOP) {
                inheritContext = (attrs instanceof XmlPullParser)
                        // If we have a XmlPullParser, we can detect where we are in the layout
                        ? ((XmlPullParser) attrs).getDepth() > 1
                        // Otherwise we have to use the old heuristic
                        : shouldInheritContext((ViewParent) parent);
            }
    
            return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
                    IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
                    true, /* Read read app:theme as a fallback at all times for legacy reasons */
                    VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
            );
        }
    
    
  • AppCompatViewInflater的createView()来创建View,其最终都转换为AppCompat对应的组件,

    
        final View createView(View parent, final String name, @NonNull Context context,
                @NonNull AttributeSet attrs, boolean inheritContext,
                boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
            final Context originalContext = context;
    
            // We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
            // by using the parent's context
            if (inheritContext && parent != null) {
                context = parent.getContext();
            }
            if (readAndroidTheme || readAppTheme) {
                // We then apply the theme on the context, if specified
                context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
            }
            if (wrapContext) {
                context = TintContextWrapper.wrap(context);
            }
    
            View view = null;
    
            // We need to 'inject' our tint aware Views in place of the standard framework versions
            //这里对View进行转换,自动转换为AppCompat对应的View。
            switch (name) {
                case "TextView":
                    view = createTextView(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "ImageView":
                    view = createImageView(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "Button":
                    view = createButton(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "EditText":
                    view = createEditText(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "Spinner":
                    view = createSpinner(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "ImageButton":
                    view = createImageButton(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "CheckBox":
                    view = createCheckBox(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "RadioButton":
                    view = createRadioButton(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "CheckedTextView":
                    view = createCheckedTextView(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "AutoCompleteTextView":
                    view = createAutoCompleteTextView(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "MultiAutoCompleteTextView":
                    view = createMultiAutoCompleteTextView(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "RatingBar":
                    view = createRatingBar(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "SeekBar":
                    view = createSeekBar(context, attrs);
                    verifyNotNull(view, name);
                    break;
                case "ToggleButton":
                    view = createToggleButton(context, attrs);
                    verifyNotNull(view, name);
                    break;
                default:
                    // The fallback that allows extending class to take over view inflation
                    // for other tags. Note that we don't check that the result is not-null.
                    // That allows the custom inflater path to fall back on the default one
                    // later in this method.
                    view = createView(context, name, attrs);
            }
    
            //如果不是以上对应的View,则调用以下方法进行创建。
            if (view == null && originalContext != context) {
                // If the original context does not equal our themed context, then we need to manually
                // inflate it using the name so that android:theme takes effect.
                view = createViewFromTag(context, name, attrs);
            }
    
            if (view != null) {
                // If we have created a view, check its android:onClick
                checkOnClickListener(view, attrs);
            }
    
            return view;
        }
    
  • AppCompatViewInflater的createViewFromTag()方法,其与LayoutInflater的createViewFromTag方法有些类似,

        private View createViewFromTag(Context context, String name, AttributeSet attrs) {
            if (name.equals("view")) {
                name = attrs.getAttributeValue(null, "class");
            }
    
            try {
                mConstructorArgs[0] = context;
                mConstructorArgs[1] = attrs;
    
                if (-1 == name.indexOf('.')) {
                    //尝试增加不同的前缀进行创建,用反射,如果 反射失败则加载不成功,再重新尝试。
                    for (int i = 0; i < sClassPrefixList.length; i++) {
                        final View view = createViewByPrefix(context, name, sClassPrefixList[i]);
                        if (view != null) {
                            return view;
                        }
                    }
                    return null;
                } else {
                    return createViewByPrefix(context, name, null);
                }
            } catch (Exception e) {
                // We do not want to catch these, lets return null and let the actual LayoutInflater
                // try
                return null;
            } finally {
                // Don't retain references on context.
                mConstructorArgs[0] = null;
                mConstructorArgs[1] = null;
            }
        }
    
    
  • 尝试用不同的前缀反射创建View,到了这里View就创建成功了。

        private View createViewByPrefix(Context context, String name, String prefix)
                throws ClassNotFoundException, InflateException {
            Constructor<? extends View> constructor = sConstructorMap.get(name);
    
            try {
                if (constructor == null) {
                    // Class not found in the cache, see if it's real, and try to add it
                    Class<? extends View> clazz = Class.forName(
                            prefix != null ? (prefix + name) : name,
                            false,
                            context.getClassLoader()).asSubclass(View.class);
    
                    constructor = clazz.getConstructor(sConstructorSignature);
                    //为了增加速度,这里也尝试了缓存的方式。
                    sConstructorMap.put(name, constructor);
                }
    
                constructor.setAccessible(true);
                return constructor.newInstance(mConstructorArgs);
            } catch (Exception e) {
                // We do not want to catch these, lets return null and let the actual LayoutInflater
                // try
                return null;
            }
        }
    

LayoutInflater设置Factory2

  • 在onCreateView中设置Factory2,通过回调函数我们能拿到View的名称(注意,我们连系统的根布局的名称也是可以拿到的),同事也可以拿到其对应的属性值,这样我们就可以根据这些值来创建我们的View,在这里也可以动态的设置我们想要的皮肤。

     @Override
        protected void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
            getLayoutInflater().setFactory2(new LayoutInflater.Factory2() {//这里主要负责View的创建。
                @Override
                public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
                    Log.e(TAG, "name: " + name);
    
                    int attributeCount = attrs.getAttributeCount();
                    for (int i = 0; i < attributeCount; i++) {
                        String attributeName = attrs.getAttributeName(i);
                        Log.e(TAG, "attributeName: " + attributeName);
                    }
    
                    Log.e(TAG, "------------------------------------divide line------------------- ");
    
                    return null;
                }
    
                @Override
                public View onCreateView(String name, Context context, AttributeSet attrs) {
                    return null;
                }
            });
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_happy);
        }
    

    日志打印:

    name: LinearLayout
    attributeName: orientation
    attributeName: fitsSystemWindows
    attributeName: layout_width
    attributeName: layout_height
    ------------------------------------divide line------------------- 
    name: ViewStub
    attributeName: theme
    attributeName: id
    attributeName: layout
    attributeName: inflatedId
    attributeName: layout_width
    attributeName: layout_height
    ------------------------------------divide line------------------- 
    name: FrameLayout
    attributeName: id
    attributeName: layout_width
    attributeName: layout_height
    attributeName: foreground
    attributeName: foregroundGravity
    attributeName: foregroundInsidePadding
    ------------------------------------divide line------------------- 
    name: androidx.appcompat.widget.FitWindowsLinearLayout
    attributeName: orientation
    attributeName: id
    attributeName: fitsSystemWindows
    attributeName: layout_width
    attributeName: layout_height
    ------------------------------------divide line------------------- 
    name: androidx.appcompat.widget.ViewStubCompat
    attributeName: id
    attributeName: layout
    attributeName: inflatedId
    attributeName: layout_width
    attributeName: layout_height
    ------------------------------------divide line------------------- 
    name: androidx.appcompat.widget.ContentFrameLayout
    attributeName: id
    attributeName: layout_width
    attributeName: layout_height
    attributeName: foreground
    attributeName: foregroundGravity
    ------------------------------------divide line------------------- 
    name: LinearLayout
    attributeName: orientation
    attributeName: layout_width
    attributeName: layout_height
    ------------------------------------divide line------------------- 
    

实现方式

我们可以以apk的方式来打包资源,这个apk资源包中只存在资源文件,然后我们通过动态加载技术来加载apk资源包,实现动态换肤的功能。

LayoutInflater源码总结

  1. Activity与AppCompatActivity都是调用LayoutInflater进行View的创建,但是AppCompatActivity的View是用Factory2进行创建的,我们可以用这种机制来实现替换动态换肤的实现。
  2. LayoutInflater的Factory2是带有重设校验的,它是不支持重复设置参数的,我们可以有两种方式来设置我们的Factory2。
    • 在Activity的的surper.onCrate()方法调用setFactory2的方法设置Factory2.
    • 通过反射设置Factory2 的值。
  3. 在LayoutInflater里面有个1995闪烁layout,我们可以通过在标签中使用blink标签,来达到闪烁布局的效果。
  4. 创建View是通过反射进行创建的,为了加快构造方法的创建速度,将之前生成的构造方法进行缓存。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/8469.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

高级UI——Paint(滤镜,颜色通道,矩阵运算)

前言 我们已经详细了解到整个android程序&#xff0c;从启动再到绘制的整体流程&#xff0c;从这中间我们又牵扯出了Canvas绘制图形的画板和我们的Paint控制色彩样式的画笔&#xff0c;那么之前基础篇我们就不进行详细的解释&#xff0c;那些API在之前的基础篇已经公布出来&am…

Typescript 函数类型详解

Typescript 函数 前言 虽然 JS/TS 支持面向对象编程&#xff0c;但大部分时候还是在写函数。函数是一等公民。本文介绍下如何在 TypeScript 中使用函数&#xff0c;包括&#xff1a; 函数类型声明函数参数类型&#xff1a;可选参数、默认参数、剩余参数函数返回值类型this 类…

java#5(数组)

目录 数组 1.数组的完整格式:数据类型[] 数组名 new 数据类型[]{元素1,元素2......}; 2.数组的简化格式:数据类型[] 数组名 {元素1,元素2......}; 3.数组的地址​编辑 4.数组的索引(下标,角标) 5.length的使用(表示数组的长度:有几个元素) 6.数组动态初始化:初始化时指…

Redis事务入门及命令

文章目录Redis 事务入门及命令事务概念Redis 事务概念Redis 事务特性Redis 三个阶段入门代码示例Redis 相关命令MULTIDISCARDEXECWATCHUNWATCHRedis 事务入门及命令 事务概念 数据库事务( transaction )是访问并可能操作各种数据项的一个数据库操作序列&#xff0c;这些操作要…

详解 YUV,一文搞定 YUV 是什么!

YUV 是一个颜色模型&#xff0c;通常用作彩色图像管道的一部分。它对彩色图像或视频进行编码时考虑到了人类的感知&#xff0c;与“直接”的 RGB 表示相比&#xff0c;允许减少色度分量的带宽。历史上&#xff0c;术语 YUV 和 Y’UV 用于电视系统中颜色信息的特定模拟编码。今天…

HTML学生作业网页:使用HTML+CSS技术实现传统文化网页设计题材-西安事变历史纪念馆 10页 带视频 带音乐

Web前端开发技术 描述 网页设计题材&#xff0c;DIVCSS 布局制作,HTMLCSS网页设计期末课程大作业 | 茶文化网站 | 中华传统文化题材 | 京剧文化水墨风书画 | 中国民间年画文化艺术网站 | HTML期末大学生网页设计作业 HTML&#xff1a;结构 CSS&#xff1a;样式 在操作方面上运…

《上海悠悠接口自动化平台》-4.注册用例集实战演示

前言 以注册接口为例&#xff0c;在平台上演示如何维护接口自动化用例 访问地址http://47.108.155.10/login.html 用户名: demo, 密码: demo123 有兴趣的可以自己去查看用例规范 和 运行效果。 API 接口层 先找出注册接口的接口文档&#xff0c;以下是接口文档部分 主要关…

Redis配置哨兵及其机制

目录一、Redis哨兵诞生背景二、关于哨兵三、哨兵机制的基本流程3.1 监控3.2 选主3.3 通知四、关于主观下线和客观下线4.1 主观下线4.2 客观下线五、选主规则3.1 优先级最高的从库得分高3.2 和旧主库同步程度最接近的从库得分高3.3 ID 号小的从库得分高六、配置流程七、总结一、…

网络面试-0x10地址栏输入URL敲入回车后发生了什么?

一、 URL解析 1、 首先判断你输入的是一个合法的URL还是待搜索的关键字 2、如果是URL&#xff0c;对URL进行解析 二、 DNS查询 1、设备 —— 本地DNS服务器 —— xx 递归过程 2、DNS服务器和 顶级域名服务器、二级域名服务器、权威域名服务器之间是迭代过程。 三、 TCP连接 …

redis和selery相关知识点

目录标题一&#xff1a;redis字符串操作二&#xff1a;redis hash操作三&#xff1a;redis列表操作四&#xff1a;redis管道1.redis数据库&#xff0c;是否支持事务&#xff1f;2.redis代码实现事务五&#xff1a;redis其他操作六&#xff1a;django中集成redis1.方式一:直接使…

用python就获取到照片拍摄时的详细位置【源码公开】

文章目录一.引言1.读取照片信息&#xff0c;获取坐标2.通过baidu Map的API将GPS信息转换成地址。二.源码附上&#xff01;&#xff01;&#xff01;注意事项一.引言 先看获取到的效果 拍摄时间&#xff1a;2021:12:18 16:22:13 照片拍摄地址:(内蒙古自治区包头市昆都仑区, 内…

pytorch从零开始搭建神经网络

目录 基本流程 一、数据处理 二、模型搭建 三、定义代价函数&优化器 四、训练 附录 nn.Sequential nn.Module model.train() 和 model.eval() 损失 图神经网络 基本流程 《PyTorch深度学习实践》完结合集_哔哩哔哩_bilibili1. 数据预处理&#xff08;Dataset、…

由浅入深,一起来刷Java高级开发岗面试指南,明年面试必定无忧!

前言 我只想面个CV工程师&#xff0c;面试官偏偏让我挑战造火箭工程师&#xff0c;加上今年这个情况更是前后两男&#xff0c;但再难苟且的生活还要继续&#xff0c;饭碗还是要继续找的。在最近的面试中我一直在总结&#xff0c;每次面试回来也都会复盘&#xff0c;下面是我根…

为啥50岁以后,病就增多了?中老年人想要少生病,该做些什么?

人到中年&#xff0c;生活会有很多变化&#xff0c;很多男性朋友从以前别人口中的小伙子&#xff0c;变成现在家里的顶梁柱&#xff0c;很多以前别人口中的小姑娘&#xff0c;变成现在的贤妻良母&#xff0c;或者拥有自己的一番事业。角色在变化的同时&#xff0c;身体情况也发…

高压电气系统验证

纯电和混合动力汽车中的高压电气系统关乎整车的能耗和安全&#xff0c;需要在部件及整车开发阶段做全面的测试与验证。符合ISO 21498*标准的电压、电流一体式测量模块CSM HV BM系列产品&#xff0c;可以直接串联在整车级别的高压电气线缆中&#xff0c;安全可靠的完成高压电气系…

java面试强基(2)

字符型常量和字符串常量的区别? 形式 : 字符常量是单引号引起的一个字符&#xff0c;字符串常量是双引号引起的 0 个或若干个字符。 含义 : 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)。 占内存大小 &…

SpringCloud 核心组件Feign【远程调用自定义配置】

目录 1&#xff0c;Feign远程调用 1.1&#xff1a;Feign概述 1.2&#xff1a;Feign替代RestTemplate 1&#xff09;&#xff1a;引入依赖 2&#xff09;&#xff1a;添加注解 3&#xff09;&#xff1a;编写Feign的消费服务&#xff0c;提供服务 4&#xff09;&#xff1a;测…

C. Discrete Acceleration(浮点二分)

Problem - 1408C - Codeforces 题意: 有一条长度为l米的道路。路的起点坐标为0&#xff0c;路的终点坐标为l。 有两辆汽车&#xff0c;第一辆站在路的起点&#xff0c;第二辆站在路的终点。它们将同时开始行驶。第一辆车将从起点开到终点&#xff0c;第二辆车将从终点开到起…

通俗易懂的React事件系统工作原理

前言 React 为我们提供了一套虚拟的事件系统&#xff0c;这套虚拟事件系统是如何工作的&#xff0c;笔者对源码做了一次梳理&#xff0c;整理了下面的文档供大家参考。 在 React事件介绍 中介绍了合成事件对象以及为什么提供合成事件对象&#xff0c;主要原因是因为 React 想…

【附源码】Python计算机毕业设计图书商城购物系统

项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等等。 环境需要 1.运行环境&#xff1a;最好是python3.7.7&#xff0c;我…