我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Android视图加载到显示(基于API 29)分析

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Android视图加载到显示(基于API 29)分析

Activity视图从创建到显示 就算是咸鱼也要做最咸的那条。 没错,入口点当然是onCreate()中的SetContentView(R.layout.xxx) 调用mWindow.setContentView(R.layout.xxx),mWindow是Activity被创建时在attach()中创建的PhoneWindow对象. PhoneWindow中生成DecorView,具体是new了一个DecorView,这个DecorView是一个FrameLayout,即viewGroup. ViewGroup mContentParent = decorView.findViewById(com.android.internal.R.id.content) 下一步 LayoutInflate.inflate(R.layout.xxx,mContentParent),成功将我们传入的布局,加入到名为content的这个布局之中。 LayoutInflate是如何工作的?或者说LayoutInflate.inflate(R.layout.xx,root,true/false),这几个参数有啥效果?

LayoutInflate是如何创建的?

mLayoutInflater = LayoutInflater.from(context);

  //可见,LayoutInflate是一个服务,因为要加载app里面的资源,当然需要用服务去搞事情。
  public static LayoutInflater from(Context context) {
  LayoutInflater LayoutInflater =
          (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
  if (LayoutInflater == null) {
      throw new AssertionError("LayoutInflater not found.");
  }
  return LayoutInflater;}

常见的添加布局是如何搞的?

mLayoutInflater.inflate(layoutResID, mContentParent);

 public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
 //这边传入的就是 id,ViewParent,true.
 return inflate(resource, root, root != null); }
 final Resources res = getContext().getResources();
 XmlResourceParser parser = res.getLayout(resource);
   try {
		 return inflate(parser, root, attachToRoot);
  } finally {
 	  parser.close();
  }
  //这个res的实现是ResoursesImpl,其中使用了AssertManager去获取这个布局,先确定这个布局资源是存在的,然后,加载这个资源布局
  XmlResourceParser parser = res.getLayout(resource);
 final ResourcesImpl impl = mResourcesImpl;
 impl.getValue(id, value, true);
 if (value.type == TypedValue.TYPE_STRING) {
     return impl.loadXmlResourceParser(value.string.toString(), id,
             value.assetCookie, type);
 //loadXmlResourceParser
 //load这个xml资源的时候,ResourcesImpl中有一个大小为4的数组,用于缓存
 //native 方法去寻找这个资源布局
     final XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file);
         if (block != null) {
 			//缓存处理
             final int pos = (mLastCachedXmlBlockIndex + 1) % num;

inflate(parser, root, attachToRoot); 已经找到这个资源文件,并且转换成XmlResourceParser,下一步,inflate

  public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
  synchronized (mConstructorArgs) {
      final AttributeSet attrs = Xml.asAttributeSet(parser);
      View result = root;
      try {
          advanceToRootNode(parser);
          final String name = parser.getName();
          if (TAG_MERGE.equals(name)) {
              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) {
  				//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);
                  }
              }
  			//开始解析指定布局的xml文件
              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) {
  				//布局文件生成的View添加进根Root
                  root.addView(temp, params);
              }
  			// 仅仅是根据布局文件生成View,就返回这个View
              if (root == null || !attachToRoot) {
                  result = temp;
              }
          }
      return result;
  }

}

rInflateChildren() 调用了rInflate()
  void rInflate(XmlPullParser parser, View parent, Context context,
      AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
  final int depth = parser.getDepth();
  int type;
  boolean pendingRequestFocus = false;
  //解析tag的循环
  while (((type = parser.next()) != XmlPullParser.END_TAG ||
          parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
      if (type != XmlPullParser.START_TAG) {
          continue;
      }
      final String name = parser.getName();
 		 ...else if (TAG_INCLUDE.equals(name)) {
          if (parser.getDepth() == 0) {
              throw new InflateException(" cannot be the root element");
  		//解析我们经常使用的include标签
  			 parseInclude(parser, context, parent, attrs);
          }
          else{
  		//解析xml文件里面的控件
          final View view = createViewFromTag(parent, name, context, attrs);
          final ViewGroup viewGroup = (ViewGroup) parent;
          final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
  		//本身外层是while循环,而这下一句,产生递归效果,因为,可能这个节点是一个ViewGroup,那么就需要进去遍历
          rInflateChildren(parser, view, attrs, true);
          viewGroup.addView(view, params);
      }
  }	
  //注意这个onFinishInflate(),它代表了布局解析完毕,自定义ViewGroup,有时就会用到这个方法。
  if (finishInflate) {
      parent.onFinishInflate();
  }

}

createViewFromTag()

   View view = tryCreateView(parent, name, context, attrs);
  //以下为tryCreateView()的代码
  if (mFactory2 != null) {
  	//这个Factory为LayoutInflate的一个接口,返回的是个View,也就是更具名字生成View,具体怎么生成,这个过程交给了这个工厂,我们去找一下在哪里实现的
      view = mFactory2.onCreateView(parent, name, context, attrs);
  } else if (mFactory != null) {
      view = mFactory.onCreateView(name, context, attrs);
  } else {
      view = null;
  }
  return view;}

寻找Factory在哪里初始化进来的

  //同时我们在LayotInflate中还发现了setFactory()和setFactory2()来设置的方法。
  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;

找了不一会儿,我们发现AppCompatActivity,里面的:

   @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
  final AppCompatDelegate delegate = getDelegate();
  //这里installViewFactory()
  delegate.installViewFactory();
  delegate.onCreate(savedInstanceState);
  //在onCreate之前设置Factory, 所以,你想根据自己的规则创建View,你需要在onCreate()的super之前设置就没问题了。
  super.onCreate(savedInstanceState);
  }
   @Override
   public void installViewFactory() {
  LayoutInflater layoutInflater = LayoutInflater.from(mContext);
  if (layoutInflater.getFactory() == null) {
  	//为空才设置,也就是我们自己可以创建自己的Factory,自己去创建View
      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");
      	}}}
  public AppCompatDelegate getDelegate() {
  if (mDelegate == null) {
  //创建了 AppCompatDelegateImpl
      mDelegate = AppCompatDelegate.create(this, this);
  }
  return mDelegate;
  }
  //而这个AppCompatDelegateImpl实现了LayoutInflater.Factory2接口
   
  @Override
  public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
      return createView(parent, name, context, attrs);
  }

onCreateView中创建了一个类:AppCompatViewInflate,并且 mAppCompatViewInflater.createView(),来到这个createView看一看:

    switch (name) {
      case "TextView":
  		// new AppCompatTextView(context, attrs),new 出来了我们需要用的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;
  ........
  //创建我们的自定View,所以自定义View需要写上全路径,因为需要用到反射。
  if (view == null && originalContext != context) {
      view = createViewFromTag(context, name, attrs);
  }
  if (view != null) {
      // If we have created a view, check its android:onClick
      checkOnClickListener(view, attrs);
  }
  return view;
那么解释我们经常使用LayoutInfalte.inflate(R.layout.xxx,root,true/faalse)第二个参数和第三个参数的意义下面这几行代码就差不多了:
   // 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;}
到此,我们的setContView就差不多了, 分析了传入布局 生成DecorView 将布局使用LayoutInflate.inflate添加进R.id.content的一些过程。 View显示过程。假装你已经知道Activity的生命周期,并且也知道代码在哪里执行。那我们直接来到应用入口点的那个类。 小明同学,请等等····是哪个类??呜,那就告诉你吧,是ActivityThread

来到执行resume的方法:handleResumeActivity()

  @Override
  public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
      String reason) {
  //onNewIntent(),以及调用Activity的resume()生命周期函数,都在下面这个方法执行
  final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
  ...
  //获取到PhoneWindow,
   r.window = r.activity.getWindow();
  //获取DecorView
      View decor = r.window.getDecorView();
  //decorView设置为不可见
      decor.setVisibility(View.INVISIBLE);
  //拿到windowManagerImpl
      ViewManager wm = a.getWindowManager();
      WindowManager.LayoutParams l = r.window.getAttributes();
      a.mDecor = decor;
      l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
      l.softInputMode |= forwardBit;
  ...
  if (!a.mWindowAdded) {
      a.mWindowAdded = true;
  	//WindowManagerImpl的addView方法,传入参数为decorView,和Window的属性
  	//故事就从这里开始了
      wm.addView(decor, l);
  ...	
  //调用Activity里面的makeVisisble()使得视图可见
   if (r.activity.mVisibleFromClient) {
          r.activity.makeVisible();
      }
  //你说了解Handler,那来说说IdleHandler吧。
   Looper.myQueue().addIdleHandler(new Idler());

ok,可以看到上面一系列“熟悉”的流程。我们重点关注上面的wm.addView(DecorView,WindowManager.LayoutParams). 进入WindowManagerImpl.

  @Override
  public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
      applyDefaultToken(params);
  	//单例的WindowMagerGlobal.
      mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
  }

WindowManagerGlobal

  	//创建ViewRootImpl
      root = new ViewRootImpl(view.getContext(), display);
      view.setLayoutParams(wparams);
  	//集合缓存
      mViews.add(view);
      mRoots.add(root);
      mParams.add(wparams);
      // do this last because it fires off messages to start doing things
  	//调用ViewRootImpl的setView方法:
      root.setView(view, wparams, panelParentView);

来到ViewRootImpl的setView,很快我们看到了一个名为requestLayout的方法

  @Override
  public void requestLayout() {
      if (!mHandlingLayoutInLayoutRequest) {
          checkThread();
          mLayoutRequested = true;
          scheduleTraversals();
      }
  }
  //上面的checkThread
  void checkThread() {
  还是那个熟悉的提示,原来这么多年来,提示非主线程不能更新Ui的提示,都在这里孤孤单单,今天我终于来看她了。
      if (mThread != Thread.currentThread()) {
          throw new CalledFromWrongThreadException(
                  "Only the original thread that created a view hierarchy can touch its views.");
      }
  }
  //上面的scheduleTraversals()
  void scheduleTraversals() {
  if (!mTraversalScheduled) {
      mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
      mChoreographer.postCallback(
              Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
   }}
  注意这个mTraversalRunnable
      final class TraversalRunnable implements Runnable {
  @Override
  public void run() {
      doTraversal(); //调用了doThrversal()
  }}
doThaversal()中调用了performTraversals() 好的,我们的故事开始了~~

performTraversals中一直往下走,走啊走,你会看到一行代码:

  // Ask host how big it wants to be
  performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);	

再走:

   performLayout(lp, mWidth, mHeight);	

再接再厉

   performDraw();

完毕之后,我们看

  private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
      if (mView == null) {
          return;
      }
      Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
      try {
  	//这个mView就是DecorView
          mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
      } finally {
          Trace.traceEnd(Trace.TRACE_TAG_VIEW);
      }
  }

DecorView也是继承自View,来到View中的measure(),我们看到:

  调用了onMeasure,decorView的父类是FragmLayout
  onMeasure(widthMeasureSpec, heightMeasureSpec);

FrameLayout##onMeasure()

  @Override
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
  int count = getChildCount();
  int maxHeight = 0;
  int maxWidth = 0;
  int childState = 0;
  //注意到,开始循环遍历ViewGroup里面包含的子View
  for (int i = 0; i < count; i++) {
      final View child = getChildAt(i);
  	//不为Gone的不去测算,为InVISibility也需要测算的
      if (mMeasureAllChildren || child.getVisibility() != GONE) {
          measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);}
  .......
  setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
          resolveSizeAndState(maxHeight, heightMeasureSpec,
                  childState << MEASURED_HEIGHT_STATE_SHIFT));

measureChildWithMargins()

  protected void measureChildWithMargins(View child,
      int parentWidthMeasureSpec, int widthUsed,
      int parentHeightMeasureSpec, int heightUsed) {
  //拿到孩子写在布局里面的宽高属性
  final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
  //测算子孩子的大小
  final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
          mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                  + widthUsed, lp.width);
  final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
          mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                  + heightUsed, lp.height);
  //看到再次调用child.measure(),measure中会调用onMeasure()
  child.measure(childWidthMeasureSpec, childHeightMeasureSpec);}

View的Measure过程

由父类和子类确定子类的模式和大小。

  public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
  int specMode = MeasureSpec.getMode(spec);
  int specSize = MeasureSpec.getSize(spec);
  int size = Math.max(0, specSize - padding);
  int resultSize = 0;
  int resultMode = 0;
  switch (specMode) {
  // Parent has imposed an exact size on us
  case MeasureSpec.EXACTLY:
      if (childDimension >= 0) {
          resultSize = childDimension;
          resultMode = MeasureSpec.EXACTLY;
      } else if (childDimension == LayoutParams.MATCH_PARENT) {
          // Child wants to be our size. So be it.
          resultSize = size;
          resultMode = MeasureSpec.EXACTLY;
      } else if (childDimension == LayoutParams.WRAP_CONTENT) {
          // Child wants to determine its own size. It can't be
          // bigger than us.
          resultSize = size;
          resultMode = MeasureSpec.AT_MOST;
      }
      break;
  // Parent has imposed a maximum size on us
  case MeasureSpec.AT_MOST:
      if (childDimension >= 0) {
          // Child wants a specific size... so be it
          resultSize = childDimension;
          resultMode = MeasureSpec.EXACTLY;
      } else if (childDimension == LayoutParams.MATCH_PARENT) {
          // Child wants to be our size, but our size is not fixed.
          // Constrain child to not be bigger than us.
          resultSize = size;
          resultMode = MeasureSpec.AT_MOST;
      } else if (childDimension == LayoutParams.WRAP_CONTENT) {
          // Child wants to determine its own size. It can't be
          // bigger than us.
          resultSize = size;
          resultMode = MeasureSpec.AT_MOST;
      }
      break;
  // Parent asked to see how big we want to be
  case MeasureSpec.UNSPECIFIED:
      if (childDimension >= 0) {
          // Child wants a specific size... let him have it
          resultSize = childDimension;
          resultMode = MeasureSpec.EXACTLY;
      } else if (childDimension == LayoutParams.MATCH_PARENT) {
          // Child wants to be our size... find out how big it should
          // be
          resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
          resultMode = MeasureSpec.UNSPECIFIED;
      } else if (childDimension == LayoutParams.WRAP_CONTENT) {
          // Child wants to determine its own size.... find out how
          // big it should be
          resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
          resultMode = MeasureSpec.UNSPECIFIED;
      }
      break;
  }
  //noinspection ResourceType
  return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
  }

父布局是Exactly

子布局如果为AT_MOST(wrap_content), 那么模式为AT_MOST,其他都是Exactly

父布局是AT_MOST

子View写为准确值(100dp),那么模式是Exactly

子View是wrap_content或match_parent,都是AT_MOST模式。

上面两句话读起来有点晦涩难懂,记住Exactly是已经确定了,AT_MOST就是待计算。

父类确定(Exactly)了,子类确定(100dp和match_parent)那么子类也是确定(Exactly)

父类不确定(AT_MOST), 子类确定(100dp)那么是Exactly,否则,子类都不确定。

举一个小栗子,就是ScrollView包裹ListView ,你会发现只显示了一条数据。网上给出的解决方案很多都是: 计算ListView 的高度,然后更改List的LayoutParams。竟然把所有的ListView所有的孩子都拿来累加高度。

这个问题是因为ScrollView,传过来的模式是:UNSPECIFIED,ListView中的onMeasure,对于UNSPECIFIED的处理是:

if (heightMode == MeasureSpec.UNSPECIFIED) {
        heightSize = mListPadding.top + mListPadding.bottom + childHeight +
                getVerticalFadingEdgeLength() * 2;
    }

所以正确的打开方式:

//继承一下ListView,然后,重写onMeasure()方法:
val height = MeasureSpec.makeMeasureSpec(heightMeasureSpec, MeasureSpec.AT_MOST)
super.onMeasure(widthMeasureSpec, height)
另外,我们还看到有performLayout()和performDraw()方法,调用也都是大同小异,完成各自的功能。 performMeasure – > measure – > onMeasure performLayout – > layout – > layout performDraw – > draw – > drawbackground – > ondraw.
作者:聪明的殷先生


免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Android视图加载到显示(基于API 29)分析

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Android视图加载到显示(基于API 29)分析

Activity视图从创建到显示 就算是咸鱼也要做最咸的那条。 没错,入口点当然是onCreate()中的SetContentView(R.layout.xxx) 调用mWindow.setContentView(R.layout.xxx)
2022-06-06

编程热搜

  • Android:VolumeShaper
    VolumeShaper(支持版本改一下,minsdkversion:26,android8.0(api26)进一步学习对声音的编辑,可以让音频的声音有变化的播放 VolumeShaper.Configuration的三个参数 durati
    Android:VolumeShaper
  • Android崩溃异常捕获方法
    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面。而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里。但平时使用的时候给你闹崩溃,那你就欲哭无泪了。 那么今天主要讲一下如何去捕捉系统出现的U
    Android崩溃异常捕获方法
  • android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
    系统的设置–>电池–>使用情况中,统计的能耗的使用情况也是以power_profile.xml的value作为基础参数的1、我的手机中power_profile.xml的内容: HTC t328w代码如下:
    android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
  • Android SQLite数据库基本操作方法
    程序的最主要的功能在于对数据进行操作,通过对数据进行操作来实现某个功能。而数据库就是很重要的一个方面的,Android中内置了小巧轻便,功能却很强的一个数据库–SQLite数据库。那么就来看一下在Android程序中怎么去操作SQLite数
    Android SQLite数据库基本操作方法
  • ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
    工作的时候为了方便直接打开编辑文件,一些常用的软件或者文件我们会放在桌面,但是在ubuntu20.04下直接直接拖拽文件到桌面根本没有效果,在进入桌面后发现软件列表中的软件只能收藏到面板,无法复制到桌面使用,不知道为什么会这样,似乎并不是很
    ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
  • android获取当前手机号示例程序
    代码如下: public String getLocalNumber() { TelephonyManager tManager =
    android获取当前手机号示例程序
  • Android音视频开发(三)TextureView
    简介 TextureView与SurfaceView类似,可用于显示视频或OpenGL场景。 与SurfaceView的区别 SurfaceView不能使用变换和缩放等操作,不能叠加(Overlay)两个SurfaceView。 Textu
    Android音视频开发(三)TextureView
  • android获取屏幕高度和宽度的实现方法
    本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下: 我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现。下面就介绍讲一讲如何获取屏幕的物理尺寸 下面的代码即
    android获取屏幕高度和宽度的实现方法
  • Android自定义popupwindow实例代码
    先来看看效果图:一、布局
  • Android第一次实验
    一、实验原理 1.1实验目标 编程实现用户名与密码的存储与调用。 1.2实验要求 设计用户登录界面、登录成功界面、用户注册界面,用户注册时,将其用户名、密码保存到SharedPreference中,登录时输入用户名、密码,读取SharedP
    Android第一次实验

目录