11.《android编程权威指南》笔记一

一、Android开发初体验

  监听器使用匿名内部类的好处:1,因为匿名内部类的使用,我们可在同一处实现监听器方法,代码更清晰可读。2,事件监听器一般只在同一处使用,使用匿名内部类可避免不必要的命名类实现。

二、Android与MVC设计模式
模型对象存储着应用的数据和业务逻辑。模型类通常用来映射与应用相关的一些事物,如用户、商店里的商品、服务器上的图片或者一些段电视节目,抑或GeoQuiz应用里的地理知识问题。模型对象不关心用户界面,它存在的唯一上的就是存储和管理应用数据。
视图对象知道如何在屏幕上绘制自己,以及如何响应用户的输入,如触摸动作等。一个简单经验法则是,凡是能够在屏幕上看见的对象,就是视图对象。比如xml文件。
控制器对象含有应用的逻辑单元,是视图与模型对象的联系纽带。控制器对象响应视图对象触发的种类事件,此外还管理着模型对象与视图间的数据流动。一般是Activity、Fragment或Service的一个子类。
int question = mQuestonBank[mCurrentIndex].getTextResId();
mQuestionTextView.setText(question); //setText(@StringRes int resid)的另一个重构方法
公共代码抽取方法:refactor->extract->method。
Button: android:drawableRight="@drawable...
TextView点击事件
ImageButton:android:src

三、Activity的生命周期
onCreate(内存),onStart(可视),onResume(前台),onPause(可视),onStop(内存),onDestroy,onRestart
Log快捷键:logt、logd、logm
LogCat过滤设置:选择Edit Filter Configuration选项,单击+按钮创建消息过滤器,在Filter Name处输入QuizActivity,Log Tag处同样输入QuizActivity,单击OK。现在,LogCat窗口仅显示Tag为QuizActivity的日志信息。
设备处于水平方向时,Android会找到并使用res/layout-land目录下的布局资源。
FrameLayout里面控件使用android:layout_gravity属性。
设备旋转前保存数据:onSaveInstanceState / if(savedInstanceState != null)...

四、Android应用的调试
诊断应用异常(配合逻辑诊断)
记录栈跟踪日志(查看方法在哪被调用):Log.d(TAG,"Updating question text ",new Exception());
设置断点(Debug而不是Run),运行后可检查对象,变量的值,单击this可看到超类的值。
使用异常断点(调试时会定位到异常抛出的代码行):Run --> View Breakpoints,单击+,选择Java Exception Breakpoints,输入RuntimeException并选择。异常断点影响大建议及时清除不需要的断点。
使用Android Lint(会检查项目中所有潜在问题):Analyze --> Inspect Code...,选择Whole project。
R类的问题(资源编译错误):重新检查资源文件中XML文件的有效性、清理项目、使用Gradle同步项目、运行Android Lint
布局检查器(查看布局树):androd monitor --> hierarchy View
内存分配跟踪:点击按钮启动后,在前台操作应用时,后台就开始记录内存分配状况,寻找可优化的点。

五、第二个Activity
tools和tools:text命名空间可以覆盖某个组件的任何属性。
activity调用startActivity()方法时,调用请求发送给了操作系统的ActivityManager。ActivityManager负责创建Activity实例并调用其onCreate()方法。
//CheatActivity
private static final String EXTRA_ANSWER_IS_TRUE = "com.bignerdranch.android.geoquiz.answer_is_true";

public static Intent newIntent(Context packageContext,boolean answerIsTrue) {
    Intent intent = new Intent(packageContext,CheatActivity.class);
    intent.putExtra(EXTRA_ANSWER_IS_TRUE,answerIsTrue);
    return intent;
}
//QuizActivity
Intent intent = CheatActivity.newIntent(QuizActivity.this,answerIsTrue);
            startActivity(intent);
//CheatActivity
    mAnswerIsTrue = getIntent().getBooleanExtra(EXTRA_ANSWER_IS_TRUE,false);

启动应用时,实际上是启动了应用的lanunch activity。
ActivityManager维护着一个非特定应用独享的回退栈。所有应用的activity都共享该回退栈。这也是将ActivityManager设计成操作系统级的activity管理器来负责启动应用activity的原因之一。显然,回退栈是作为一个整体共享于操作系统及设备,而不单单用于某个应用。

六、Android SDK版本与兼容
使用Android Lint,在老版本的系统上调用新版本代码时,潜在问题在编译时就能被发现。也就是说,如果使用了高版本系统API中的代码,Android Lint会提示编译错误。
根据系统版本编译:Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLTPOP
Android文档在SDK安装目录中的docs目录中。

七、UI fragment与fragment管理器
activity托管fragment:activity在其视图层级里提供一处位置,用来放置fragment视图。fragment本身没有在屏幕上显示视图的能力。
生命周期:(setContentView方法中调用)onAttach,inCreate,onCreateView,(创建)onActivityCreate,(停止)onStart,(暂停)onResume,(运行)onPause,(暂停)onStop,(停止)onDestroyView,(activity关闭)onDestroy,onDetach,销毁。
activity托管UI fragment有两种方式:在activity布局中添加fragment;在activity代码中添加fragment。一般使用第二种方式。
onCreateView(){ View v = inflater.inflate(R.layout.fragment_crime,container,false); return v;} //CrimeFragment
EditText.addTextChangedListener

FragmentManager fm = getSupportFragmentManager();
Fragment fragment = fm.findFragmentById(R.id.fragment_container); //看framelayout里面是否已经被添加过fragment。
if(fragment == null) {
  fragment = new CrimeFragment();
  fm.beginTransaction().add(R.id.fragment_container,fragment).commit();

在activity处于运行状态时,添加fragment时,FragmentManager立即驱赶fragment,调用从onAttach到onResume方法,追上activity步伐(与activity的最新状态保持同步)后,托管activity的FragmentManager就会边接收操作系统的调用指令,边调用其他生命周期方法,让fragment与activity的状态取得一致。

要合理使用fragment。应用单屏最多使用2~3个fragment。在开发中尽量使用fragment,因为后期添加太麻烦。

要使用支持库版fragment,应用的activity必须继承FragmentActivity。AppCompatActivity是FragmentActivity子类,FragmentActivity又是Activity的子类。

八、使用RecyclerView显示列表
单例是特殊的Java类,在创建实例时,一个单例类仅允许创建一个实例。应用能在内存里存活多久,单例就能存活多久。
要创建单例,需创建一个带有私有构造方法及get()方法的类。如果实例已存在,get()方法就直接返回它;如果实例还不存在,get()方法就会调用构造方法创建它。

使用抽象activity托管fragment,把通用的建立一个fragment的activity设置成超类,写一个createFragment的抽象方法。

使用RecyclerView:ViewHolder只做一件事,容纳View视图。RecyclerView自身不会创建视图,它创建的是ViewHolder,而ViewHolder引用着itemView。
  Adapter是一个控制器对象,从模型层获取数据,然后提供给RecyclerView显示,是沟通的桥梁。它负责创建必要的ViewHolder和绑定ViewHolder至模型层数据。RecyclerView需要显示视图对象时,就会去找它的Adapter。
  首先调用Adapter的getItemCount方法,询问数组列表中包含多少个对象;接着RecyclerView调用Adapter的onCreateViewHolder方法创建ViewHolder及其要显示的视图;最后,Recycler会传入ViewHolder及其位置,调用onBindViewHolder方法,Adapter会找到目标位置的数据并将其绑定到ViewHolder的视图上。所谓绑定,就是使用模型数据 填充视图。
  需要注意的是,相对于onBindViewHoler方法,onCreateViewHolder方法的调用并不频繁。一旦有了够用的ViewHolder,RecyclerView就会停止调用onCreateView方法,随后,它会回收利用旧的ViewHoler以节约时间和内存。
  Recycler类不会新版摆放屏幕上的列表项,实际上,摆放的任务被委托给了LayoutManager。LayoutManager还负责定义屏幕滚动行为。
mCrimeRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
列表滚动流畅归功于onBindViewHolder方法,任何时候,都要确保这个方法轻巧、高效。
private class CrimeHolder extends RecyclerView.ViewHolder implements View.OnClickListener{
        private Crime mCrime;

        private TextView mTitleTextView;
        private TextView mDateTextView;

        public CrimeHolder(LayoutInflater inflater,ViewGroup parent) {
            super(inflater.inflate(R.layout.list_item_crime,parent,false));
            itemView.setOnClickListener(this);

            mTitleTextView = (TextView) itemView.findViewById(R.id.crime_title);
            mDateTextView = (TextView) itemView.findViewById(R.id.crime_date);
        }

        public void bind(Crime crime) {
            mCrime = crime;
            mTitleTextView.setText(mCrime.getTitle());
            mDateTextView.setText(mCrime.getDate().toString());
        }

        @Override
        public void onClick(View v) {
            Toast.makeText(getActivity(),mCrime.getTitle() + " clicked!",Toast.LENGTH_SHORT).show();
        }
    }

    private class CrimeAdapter extends RecyclerView.Adapter {
        private List mCrimes;

        public CrimeAdapter(List crimes) {
            mCrimes = crimes;
        }

        @Override
        public CrimeHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            LayoutInflater layoutInflater = LayoutInflater.from(getActivity());

            return new CrimeHolder(layoutInflater,parent);
        }

        @Override
        public void onBindViewHolder(CrimeHolder holder, int position) {
            Crime crime = mCrimes.get(position);
            holder.bind(crime);
        }

        @Override
        public int getItemCount() {
            return mCrimes.size();
        }
    }
RecyclerView可代替ListView和GridView。
单例能方便地存储和控制模型对象。但是无法做到持久存储,也不利于单元测试(用依赖注入工具解决)。

RecyclerView ViewType:可在RecyclerView中创建不同类的列表项。
定义三种item的xml视图,对应的需要定义三种不同的ViewHolder。 然后根据不同的需求,在Adapter里面识别并运用这些ViewHolder,Adapter里面已经定义好了一些方法,只需要重写getItemViewType(int position)方法,给每个固定的position上的item返回一个固定的类型(ViewType)就能方便的表明每个item需要的ViewHolder:
class MyAdapter extends RecyclerView.Adapter {
    //User是一个自定义类,代表封装的数据类型
    private List mUsers;

    //三种不同的ViewType类型,事先用常量定义好
    public static final int VIEW_TYPE_ONE = 1;
    public static final int VIEW_TYPE_TWO = 2;
    public static final int VIEW_TYPE_THREE = 3;

    public MyAdapter(List users) {
      mUsers = users;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

      RecyclerView.ViewHolder myViewHolder = null;
    //根据不同的ViewType类型,来返回不同的ViewHolder
      switch (viewType) {

        case VIEW_TYPE_ONE:
          myViewHolder = new ViewHolderOne
              (LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_item_one, parent, false));
        break;

        case VIEW_TYPE_TWO:
          myViewHolder = new ViewHolderTwo
              (LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_item_two, parent, false));
          break;

        case VIEW_TYPE_THREE:
          myViewHolder = new ViewHolderThree
              (LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_item_three, parent, false));
          break;
      }

      return myViewHolder;
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    //根据不同的ViewType类型,进行不同的数据绑定操作
      switch (holder.getItemViewType()) {

        case VIEW_TYPE_ONE:
          ViewHolderOne holderOne = (ViewHolderOne) holder;
          holderOne.mUserAvatar.setImageResource(R.mipmap.ic_launcher);
          holderOne.mUserName.setText(mUsers.get(position).getUserName());
          holderOne.mUserInfo.setText(mUsers.get(position).getInfo());
          break;

        case VIEW_TYPE_TWO:
          ViewHolderTwo holderTwo = (ViewHolderTwo) holder;
          holderTwo.mUserAvatar.setImageResource(R.mipmap.user_avatar);
          holderTwo.mUserName.setText(mUsers.get(position).getUserName());
          holderTwo.mUserInfo.setText(mUsers.get(position).getInfo());
          break;

        case VIEW_TYPE_THREE:
          ViewHolderThree holderThree = (ViewHolderThree) holder;
          holderThree.mUserAvatar.setImageResource(R.mipmap.ic_launcher);
          holderThree.mUserName.setText(mUsers.get(position).getUserName());
          holderThree.mUserInfo.setText(mUsers.get(position).getInfo());
          break;
      }
    }

    @Override
    public int getItemCount() {
      if (null != mUsers) return mUsers.size();
      else return 0;
    }
    //重写方法,给每个position上的item返回一个固定的ViewType类型
    //如果返回的ViewType类型不固定,则会出现各种item布局变化的情况,可能还会触发bug
    @Override
    public int getItemViewType(int position) {
      return mUsers.get(position).getType();
    }
  }

九、使用布局与组件创建用户界面
ConstraintLayout:添加四个方向上的约束可摆放组件位置,组件大小有三个选择(组件自己决定wrap_content、手动调整、充满约束布局)。
选中组件,在组件的属性视图容器设置大小,有三中:固定大小|--|、包裹内容>>>、动态适应。先把两个组件全设为wrap_content,然后拖ImageView组件到crime_date下面。在预览界面,拖住ImageView组件顶部的约束柄,将其拖向ConstraintLayout组件顶部,直到约束柄变绿,并弹出Release to Create Top Constraint这样的提示时,再松开鼠标,然后依次设置下部、右部约束。在xml中形式为:
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
对两个TextView设置约束和边距,宽度为动态适应(0dp),高度为wrap_content

样式(style)是XML资源文件,含有用来描述组件行为和外观属性定义。主题是各种样式的集合,从结构上说,主题本身也是一种样式资源,只不过它的属性指向了其他样式资源。使用主题引用,可将预定义的应用主题样式添加给指定组件,并确定组件在应用中拥有正确一致的显示风格。

边距属性的默认使用值是16dp或8dp。

十、使用fragment argument
直接获取extra信息的缺点:破坏了fragment的封装,不再是可复用的构建单元。更好的做法是使用fragment argument bundle。
每个fragment实例都可附带一个Bundle对象。该bundle包含键-值对,可以像附加extra到Activity的intent中那样使用它们。一个键-值对即一个argument。
创建fragment argument:先创建Bundle对象,然后使用put方法,将argument添加到bundle中。然后调用Fragment.setArguments方法把argument bundle附加给fragment:
```
public static CrimeFragment newInstance(UUID crimeId) { //Fragment
Bundle args = new Bundle();
args.putSerializable(ARG_CRIME_ID,crimeId);

    CrimeFragment fragment = new CrimeFragment();
    fragment.setArguments(args);
    return fragment;
}

protected Fragment createFragment() { //Activity
UUID crimeId = (UUID) getIntent().getSerializableExtra(EXTRA_CRIME_ID);
return CrimeFragment.newInstance(crimeId);
}

  
    列表数据变化: mAdapter.notifyDataSetChanged();
    高效刷新:mAdapter.notifyItemChange(int);

十一、使用ViewPager
    ViewPager在某种程度上类似于RecyclerView,不过,相较于RecyclerView与Adapter间的协同工作,ViewPager与PagerAdapter间的配合要复杂得多,所以,一般使用Google提供的PagerAdapter的子类FragmentStatePagerAdapter:

mViewPager.setAdapter(new FragmentStatePagerAdapter(fragmentManager) {
@Override
public Fragment getItem(int position) {
Crime crime = mCrimes.get(position);
return CrimeFragment.newInstance(crime.getId());
}

        @Override
        public int getCount() {
            return mCrimes.size();
        }
    });

for (int i = 0;i < mCrimes.size();i++) {
if (mCrimes.get(i).getId().equals(crimeId)) {
mViewPager.setCurrentItem(i);
break;
}
}

    FragmentStatePagerAdapter在卸载不需要的fragment时会销毁它。FragmentPagerAdapter会调用事务的detach而不是remove,只是销毁了fragment的视图,而实例还保存在FragmentManager中。前者更省内存,后者适合用户需要少量固定的fragment的情形。

    当需要ViewPager托管非fragment视图时,需要自己实现PagerAdapter接口,

    不推荐使用代码方式创建视图,不过如果只需一个视图时,可用代码创建。


十二、对话框
    建议将AlertDialog封装在DialogFragment实例中使用,使用FragmentManager管理对话框,可以更灵活地显示对话框,旋转设备后对话框也不会消失:

public class DatePickerFragment extends DialogFragment {
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
return new AlertDialog.Builder(getActivity())
.setTitle(R.string.date_picker_title)
.setPositiveButton(android.R.string.ok,null)
.create();
}
}

FragmentManager manager = getFragmentManager();
DatePickerFragment dialog = new DatePickerFragment();
dialog.show(manager,DIALOG_DATE);


    同一个activity托管的fragment间的数据传递:

public static DatePickerFragment newInstance(Date date) {
Bundle args = new Bundle();
args.putSerializable(ARG_DATE,date);

    DatePickerFragment fragment = new DatePickerFragment();
    fragment.setArguments(args);
    return fragment;
}

DatePickerFragment dialog = DatePickerFragment.newInstance(mCrime.getDate());
dialog.show(manager,DIALOG_DATE);

dialog.setTargetFragment(CrimeFragment.this,REQUEST_DATE);

public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
return;
}

    if (requestCode == REQUEST_DATE) {
        Date date = (Date) data.getSerializableExtra(DatePickerFragment.EXTRA_DATE);
        mCrime.setDate(date);
        mDateButton.setText(mCrime.getDate().toString());
    }
}

private void sendResult(int resultCode,Date date) {
if (getTargetFragment() == null) {
return;
}

    Intent intent = new Intent();
    intent.putExtra(EXTRA_DATE,date);

    getTargetFragment()
            .onActivityResult(getTargetRequestCode(),resultCode,intent);
}

.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
int year = mDatePicker.getYear();
int month = mDatePicker.getMonth();
int day = mDatePicker.getDayOfMonth();
Date date = new GregorianCalendar(year,month,day).getTime();
sendResult(Activity.RESULT_OK,date);
}
})

    编写同样的代码用于全屏fragment或对话框fragment时,可选择覆盖DialogFragment.onCreateView方法,而非oncreateDialog方法,以实现不同设备上的信息呈现。


十三、工具栏
    使用AppCompat库;
      android:theme="@style/AppTheme"
      
style="@style/BeatBoxButton"   //xml中Button属性中
  样式支持继承:

  也可以采用指定父样式的方式。

  主题可以看作是样式的进货加强版,会自动应用于整个应用,主题能引用外部和资源和其他样式:

  覆盖主题属性:从现有主题往上找父主题,直到找到相关属性为止
  修改按钮属性:逐级向上查找父主题,找到buttonSytle样式,可以在Widget.AppCompat.Button中看到button的各个属性。修改BeatBosButton的父样式为Widget.AppCompat.Button。