在最前面: 在AS中新建project 时,请勾上:
This project will support instant apps
use androidx.* artifacts 避免版本依赖的问题
Example 01 UI 复习完成 -> link
dp,像素密度,设备屏幕尺寸无关的,描述控件间距离等 (记:device) sp,描述字体 大小 (记:script) px,像素,相对的绝对单元,与CSS相似等,图片等 (记:Pixel)
RelativeLayout: 相对布局 LinearLayout: 线性布局 ConstraintLayout: 约束布局
Relative Layout & Linear Layout Linear与Relative布局的区别: 如果层级多的使用RelativeLayout
LinearLayout 主要属性
android:orientation,LinearLayout方向vertical , horizontal
android:layout_gravity,相对于 该控件的父组件 ,控件本身的显示位置。仅在LinearLayout内有效,受android:orientation属性影响bottom , center
android:gravity,控件内 内容的显示位置
android:layout_weight,比重,android:orientation 相应方向的值需设为0dp android:layout_width="0dp" android:layout_weight="1"
ConstraintLayout 它的出现主要是为了解决布局嵌套过多的问题 link
添加依赖 : 在app/build.gradle文件中添加ConstraintLayout的依赖
1 implementation 'com.android.support.constraint:constraint-layout:1.1.3'
与Relative的区别:仅对相同位置/方向进行相对的约束
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout ... > <TextView android:id ="@+id/textView" android:text ="TextView_A" ... tools:layout_editor_absoluteX ="189dp" tools:layout_editor_absoluteY ="253dp" /> <TextView android:id ="@+id/textView2" android:text ="TextView_B" ... android:layout_marginBottom ="56dp" app:layout_constraintBottom_toTopOf ="@+id/textView" tools:layout_editor_absoluteX ="280dp" /> <TextView android:id ="@+id/textView3" android:text ="TextView_C" ... android:layout_marginBottom ="72dp" app:layout_constraintBottom_toBottomOf ="@+id/textView" tools:layout_editor_absoluteX ="101dp" /> </androidx.constraintlayout.widget.ConstraintLayout >
复习完成 -> link
android:id属性,声明组件ID; 后端可以通过ID值获取组件对象
@+id,创建一个新ID
@id,引用一个ID
@,引用资源
@android:,引用android下自带资源
1 2 3 4 5 6 7 8 9 10 11 <TextView android:id ="@+id/textView1" android:layout_width ="match_parent" android:layout_height ="wrap_content" android:text ="基本文本显示" /> <ImageView android:id ="@+id/imageView" android:layout_width ="match_parent" android:layout_height ="wrap_content" app:srcCompat ="@android:drawable/ic_input_add" />
基本输入组件: Buttons;Text Fields;CheckBoxes;Radio Buttons;Toggle Buttons;ImageView;EditText
other: imageview; progressBar; Seekbar; RatingBar;
Example 03 UI & Events 复习完成 -> link
后端仅基于ID名称获取组件,无法基于不同布局文件区分组件ID,也无法区分组件类型。 调用到不是当前布局上组件,运行时才能发现错误。 因此,ID的名称必须能够体现具体的完整的信息,从而避免出错。 较好的命名方法 : 布局类型 _ 布局名称 _ 组件类型 _ 组件名称: act_main_editText_username
findViewById() : 基于ID名称获取组件
Android中Callback的设计与使用 ? 理解掌握2种实现监听的方法?
匿名内部类
lambda表达式 (set language level to 8)
View.OnClickListener: onClick()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 buttonSubmit = findViewById(R.id.act_main_button_submit); buttonSubmit.setOnClickListener(new View.OnClickListener() { @Override public void onClick (View v) { String string = editTextUserName.getText().toString(); textViewUserName.setText(string); } }); buttonSubmit.setOnClickListener(v -> { String string = editTextUserName.getText().toString(); textViewUserName.setText(string); });
EditText: TextChangedListener
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 editTextNameChange = findViewById(R.id.act_main_editText_change); editTextNameChange.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged (CharSequence s, int start, int count, int after) { } @Override public void onTextChanged (CharSequence s, int start, int before, int count) { textViewNamechange.setText(s); } @Override public void afterTextChanged (Editable s) { } });
View.OnLongClickListener: onLongClick()View.OnFocusChangeListener: onFocusChange()View.OnTouchListener: onTouch()View.OnKeyListener: onKey()
…
Example 04 App Resources & R 复习完成 -> link
各文件目录放置的资源?
drawable : 图片相关的xml文件 (若图标有固定的尺寸,不需要更改,那么drawable更适合)
minmap : 图片相关的xml文件 (如果需要变大变小的,有动画的,放在mipmap中能有更高的质量)
Layout : 布局文件
value : 用于存放显示相的配置数据的定义文件,如strings.xml, style.xml, dimens.xml, arrays.xml, ids.xml等
R.java的作用与意义 ?
R.java文件自动生成,用来定义Android程序中所有各类型的资源的索引
在java程序中引用资源 R.resource_type.resource_name
在XML文件中引用资源 @[package:]type/name @drawable/icon @ 代表的是R.java类drawable 代表的是R.java中的静态内部类 drawable/icon代表静态内部类 drawable 中的静态属性 icon 如果访问的是Android系统中自带的文件,则要添加包名“Android:” android:textColor="@android:color/red"
往R.java文件中添加一条资源记录 为组件添加Id属性作为标识:@id+/name
自定义资源文件,创建字符串数组资源,添加ListView,引入自定义字符串数组至ListView显示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 自定义资源文件 mysource.xml <?xml version="1.0" encoding="utf-8"?> <resources > <string-array name ="courses" > <item > C语言</item > <item > Java语言</item > <item > 数据库原理</item > <item > 计算机网络</item > </string-array > </resources > 布局文件 activity_main.xml <ListView android:layout_width ="match_parent" android:layout_height ="match_parent" android:entries ="@array/courses" > </ListView >
定义字符串数组资源,并在代码中通过R调用
1 button = findViewById(R.id.act_main_button);
Resources Overview Providing Resources Accessing Resources Resource Types
Example 05 Activities 复习完成 -> link
Activities
Activity 是一个应用组件,用户可与其提供的屏幕进行交互(相当于一个页面) 每个 Activity 都会获得一个用于绘制其用户界面的窗口。 窗口通常会充满屏幕,但也可小于屏幕并浮动在其他窗口之上
一个应用通常由多个彼此松散联系的 Activity 组成。 一般会指定应用中的某个 Activity 为“主”Activity,即首次启动应用时呈现给用户的那个 Activity。 而且每个 Activity 均可启动另一个 Activity,以便执行不同的操作。 每次新 Activity 启动时,前一 Activity 便会停止,但系统会在堆栈(“返回栈”)中保留该 Activity。 当新 Activity 启动时,系统会将其推送到返回栈上,并取得用户焦点。 返回栈遵循基本的“后进先出”堆栈机制,因此,当用户完成当前 Activity 并按“返回”按钮时,系统会从堆栈中将其弹出(并销毁),然后恢复前一 Activity。
Declare Activities
使用activity 需要在 manifest 配置中声明: 在 <application> 添加1个 <activity> 元素
1 2 3 4 5 6 7 <manifest ... > <application ... > <activity android:name=".ExampleActivity" /> ... </application ... > ... </manifest >
回调方法
特点
onCreate()
系统会在创建 Activity 时调用此方法. 实现内初始化 Activity 的数据 . 必须在 setContentView() 中定义 Activity 所使用的的layout文件. onCreate() 完成后 下一步 就是 onStart().
onStart()
As onCreate() exits, the activity enters the Started state. the activity becomes visible to the user. This callback contains the activity’s final preparations for coming to the foreground and becoming interactive.
onPause()
when the activity loses focus and enters a Paused state. the activity will soon enter the Stopped or Resumed state. may continue to update the UI if the user is expecting the UI to update (a media player playing).
onStop()
when the activity is no longer visible to the user may happen because the activity is being destroyedd, a new activity is starting, or an existing activity is entering a Resumed state and is covering the stopped activity next callback that the system calls is either onRestart() or onDestroy()
onRestart()
when an activity in the Stopped state is about to restart restores(恢复) the state of the activity from the time that it was stopped This callback is always followed by onStart()
onDestroy()
invokes this callback before an activity is destroyed ensure that all of an activity’s resources are released
Intent在Android中的核心作用就是“跳转”,同时可以携带必要的信息,将Intent作为一个信息桥梁
explicit intent : 显示跳转到下一个活动
1 2 Intent intent = new Intent(this , SecondActivity.class); //上下文环境; 目的Activity startActivity(intent);
传递数据 : intent.putExtra(key, value) 上一个活动向下一个活动传递数据
1 2 3 4 intent.putExtra("extra_data" , "dafadfadfa" ); getIntent().getStringExtra("extra_data" );
implicit intent : specifies an action that can invoke any app on the device able to perform the action
1 2 3 4 5 6 7 8 9 10 Intent sendIntent = new Intent(); sendIntent.setAction(Intent.ACTION_SEND); sendIntent.putExtra(Intent.EXTRA_TEXT, textMessage); sendIntent.setType("text/plain" ); if (sendIntent.resolveActivity(getPackageManager()) != null ) { startActivity(sendIntent); }
Starting an Activity 内容:Activity的切换方法;切换时前后Activity经历的生命周期过程;通过Intent在跳转切换时传递参数;Bundle; 要求:实现全部生命周期回调函数,在跳转时观察activity的状态,传递并接收参数。
知识点1 :实现OnClickListener接口的三种方法
创建内部类 (麻烦)
创建一个内部类实现OnClickListener接口并重写onClick方法
主类中实现OnClickListener接口:
在主类中实现OnClickListener接口并重写onClick方法 button.setOnClickListener(this);
匿名内部类:
当按钮较少或只有一个按钮时,可以直接创建OnClickListener的匿名内部类传入按钮的setOnClickListener参数中
知识点2 :super.onCreate(savedInstanceState)
onCreate()方法其实是覆写了基类(Activity类)的onCreate方法,super.onCreate()是在调用基类中的onCreate方法。
而在子类的onCreate方法中,不能直接调用onCreate(),因为这样做会产生递归,为了解决这一问题,java用super关键字表示超类的意思,当前类就是从超类继承而来的
savedInstanceState是保存当前Activity的状态信息
如果一个非running的Activity因为资源紧张而被系统销毁,当再次启动这个Activity时,可以通过这个保存下来的状态实例,即通过saveInstanceState获取之前的信息,然后使用这些信息,让用户感觉和之前的界面一模一样,提升用户体验。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public class MainActivity extends AppCompatActivity implements View .OnClickListener { private static final String TAG = "MainActivity" ; private Button button; private EditText editText; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); Log.i(TAG, "onCreate()" ); setContentView(R.layout.activity_main); button = findViewById(R.id.act_main_button); editText = findViewById(R.id.act_main_edittext); button.setOnClickListener(this ); } @Override public void onClick (View v) { switch (v.getId()) { case R.id.act_main_button: Intent intent = new Intent(this , SecActivity.class); intent.putExtra("value" , editText.getText().toString()); startActivity(intent); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public class SecActivity extends AppCompatActivity { private static final String TAG = "SecActivity" ; private TextView textView; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_sec); Log.i(TAG, "SecActivity onCreate()" ); String value = getIntent().getStringExtra("value" ); textView = findViewById(R.id.act_sec_textview); textView.setText(value); } @Override protected void onStart () { super .onStart(); Log.i(TAG, "SecActivity onStart()" ); } @Override protected void onResume () { super .onResume(); Log.i(TAG, "SecActivity onResume()" ); } @Override protected void onPause () { Log.i(TAG, "SecActivity onPause()" ); super .onPause(); } @Override protected void onStop () { Log.i(TAG, "SecActivity onStop()" ); super .onStop(); } @Override protected void onDestroy () { Log.i(TAG, "SecActivity onDestroy()" ); super .onDestroy(); } }
复习完成 -> link
The views in the list are represented by view holder (视图持有者) objects. These objects are instances of a class you define by extending RecyclerView.ViewHolder. Each view holder is in charge of displaying a single item with a view. The RecyclerViewcreates only as many view holders as are needed to display the on-screen portion of the dynamic content, plus a few extra. As the user scrolls through the list, the RecyclerView takes the off-screen views and rebinds them to the data which is scrolling onto the screen.
create RecyclerView 配置:在对应的 build.gradle 文件中dependencies加上
1 implementation 'androidx.recyclerview:recyclerview:1.0.0'
创建RecyclerView中item布局样式
创建实体类news封装数据
实体类属性 public ? :谷歌虚拟机没有使用内联,减少损耗 故不建议使用 get/set , 直接public
创建自定义adapter
在adapter中创建viewholder (在Adapter中创建一个继承RecyclerView.ViewHolder 的静态内部类,记为VH)
adapter添加news集合属性, 添加有参构造函数
adapter继承RecyclerView.Adapter,并声明VH泛型为自定义的VH类型
在Adapter中实现3个方法
onCreateViewHolder()
这个方法主要生成 为每个Item inflater 出一个View ,但是该方法返回 的是一个ViewHolder 。该方法把View直接封装在ViewHolder中,然后我们面向的是ViewHolder这个实例,当然这个ViewHolder需要我们自己去编写
onBindViewHolder()
这个方法主要用于适配渲染数据到View 中。方法提供 给你了一viewHolder 而不是原来的convertView
getItemCount()
这个方法就类似 于BaseAdapter的getCount 方法了,即总共有多少个条目。
重写getItemCount()方法返回集合元素数量,即需要渲染的item数量 重写onBindViewHolder()方法,当视图item滚动,绑定对应数据到item中的相应控件 重写onCreateViewHolder()方法,声明item布局样式,并将view item对象,交由viewholder持有 RecyclerView默认不包含点击事件及点击动画,需手动实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 public class MainAdapter extends RecyclerView .Adapter <MainAdapter .MyViewHolder > { private static final String TAG = "MainAdapter" ; private List<News> news; public MainAdapter (List<News> news) { this .news = news; } @NonNull @Override public MyViewHolder onCreateViewHolder (@NonNull ViewGroup parent, int viewType) { Log.i(TAG, "onCreateViewHolder" ); View itemView = LayoutInflater.from(parent.getContext()) .inflate(R.layout.recyclerview_news, parent, false ); return new MyViewHolder(itemView); } @Override public void onBindViewHolder (@NonNull MyViewHolder holder, int position) { Log.i(TAG, "onBindViewHolder: " + position + "/" + news.get(position).title); holder.title.setText(news.get(position).title); holder.suttitle.setText(news.get(position).subtitle); holder.pic.setImageResource(R.mipmap.ic_launcher); } @Override public int getItemCount () { return news.size(); } static class MyViewHolder extends RecyclerView .ViewHolder { TextView title; TextView suttitle; ImageView pic; public MyViewHolder (@NonNull View itemView) { super (itemView); title = itemView.findViewById(R.id.news_title); suttitle = itemView.findViewById(R.id.news_subtitle); pic = itemView.findViewById(R.id.news_pic); } } }
RecyclerView提供了三种布局管理器 :
LinerLayoutManager 以垂直 或者水平列表 方式展示Item
GridLayoutManager 以网格 方式展示Item
StaggeredGridLayoutManager 以瀑布流 方式展示Item
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public class MainActivity extends AppCompatActivity { private RecyclerView recyclerView; private MainAdapter adapter; private Button button; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); recyclerView = findViewById(R.id.act_main_recyclerview); RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(this ); recyclerView.setLayoutManager(mLayoutManager); recyclerView.addItemDecoration(new DividerItemDecoration(this , LinearLayoutManager.VERTICAL)); adapter = new MainAdapter(listNews()); recyclerView.setAdapter(adapter); button = findViewById(R.id.act_main_button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick (View v) { Intent intent = new Intent(MainActivity.this , SecActivity.class); startActivity(intent); } }); } private List<News> listNews () { News n1 = new News(1 , "阿根廷VS波黑" , "小组赛F组 阿根廷VS波黑" ); News n2 = new News(2 , "法国VS洪都拉斯" , "小组赛E组 法国VS洪都拉斯" ); News n3 = new News(3 , "瑞士VS厄瓜多尔" , "小组赛E组 瑞士VS厄瓜多尔" ); News n4 = new News(4 , "西班牙VS荷兰" , "小组赛B组 西班牙VS荷兰" ); News n5 = new News(5 , "俄罗斯VS丹麦" , "小组赛A组 俄罗斯VS丹麦" ); News n6 = new News(6 , "巴西VS意大利" , "小组赛C组 巴西VS意大利" ); News n7 = new News(7 , "日本VS伊朗" , "小组赛D组 日本VS伊朗" ); List<News> news = new ArrayList<>(); news.add(n1); news.add(n2); news.add(n3); news.add(n4); news.add(n5); news.add(n6); news.add(n7); news.add(n1); news.add(n2); news.add(n3); news.add(n4); return news; } }
需求OnItemClicklistener
在item布局样式中添加点击波纹动画
在adapter onBindViewHolder()方法为item view设置点击监听
高级,自定义接口,实现在activity等的点击监听
1 2 3 4 //在RecyclerView.xml根元素加上属性:添加点击波纹动画 android:background="?android:attr/selectableItemBackground" android:clickable="true" android:focusable="true"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public interface OnItemClickListener { void onItemClick (View view, int position, News news) ; } @Override public void onBindViewHolder (@NonNull ViewHolder holder, final int position) { holder.title.setText(news.get(position).title); holder.suttitle.setText(news.get(position).subtitle); holder.pic.setImageResource(R.mipmap.ic_launcher); holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick (View v) { Toast.makeText(context, news.get(position).title, Toast.LENGTH_SHORT).show(); } }); }
1 2 3 4 5 6 7 8 9 button.setOnClickListener(new View.OnClickListener() { @Override public void onClick (View v) { Intent i = new Intent(SecActivity.this , ThirdActivity.class); startActivity(i); } });
SwipeRefreshLayout
1 2 3 4 5 6 7 8 9 10 11 //将RecyclerView嵌入SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout android:id ="@+id/act_third_swipe" android:layout_width ="match_parent" android:layout_height ="0dp" android:layout_weight ="1" > <androidx.recyclerview.widget.RecyclerView android:id ="@+id/act_third_recyclerview" android:layout_width ="match_parent" android:layout_height ="match_parent" /> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout >
通过Handler模拟耗时操作,添加元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private SwipeRefreshLayout swipe;swipe = findViewById(R.id.act_third_swipe); swipe.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh () { new Handler().postDelayed(() -> { swipe.setRefreshing(false ); news.add(0 , new News(1 , "阿根廷VS波黑" + news.size(), "小组赛F组 阿根廷VS波黑" )); adapter.notifyDataSetChanged(); }, 2000 ); } });
Example 07 ItemTouchHelper 复习完成 -> link
ItemTouchHelper,用来协助处理RecyclerView中item的移动/滑动/拖拽等操作
Callback内部类(非回调接口),继承实现各种操作 重写getMovementFlags()方法,决定拖拽滑动在哪个方向是被允许 重写onSwiped()方法,Item横向滑动时回调,删除item DefaultItemAnimator类定义recyclerView item操作动画 adapter添加/移除item必须通过notifyItemInserted()/notifyItemRemoved()方法,才有动画互交效果 重写onSwiped()方法可删除item,但无法删除数据,通过自定义回调接口实现 依然通过SwipeRefreshLayout下拉刷新
ItemTouchHelper使用步骤 :
自定义callback继承 ItemTouchHelper.Callback -> 在内部写一个数据操作接口 -> 重写触发方法调用接口 -> 适配器实现数据操作接口,重新方法 -> acctivity 中附着 recyclerview
创建 MyCallback 继承 ItemTouchHelper.Callback
把数据操作部分抽象成一个接口 AdapterCallback,这个接口可以写在 MyCallback 内
因为ItemTouchHelper在完成触摸的各种动作后,就要对Adapter的数据进行操作,比如侧滑删除操作
自定义 adapter 实现接口MyCallback.AdapterCallback, 重写相关方法
在MainActivity中
1 2 ItemTouchHelper helper = new ItemTouchHelper(new MyCallback(adapter)); helper.attachToRecyclerView(recyclerView);
具体实现:
MyCallback 及内部接口 AdapterCallback
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 public class MyCallback extends ItemTouchHelper .Callback { private static final String TAG = "MyCallback" ; private AdapterCallback adapterCallback; public interface AdapterCallback { void remove (int position) ; } public MyCallback (AdapterCallback adapterCallback) { this .adapterCallback = adapterCallback; } @Override public int getMovementFlags (@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) { int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN; int swipeFlags = ItemTouchHelper.LEFT; return makeMovementFlags(0 , swipeFlags); } @Override public boolean onMove (@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) { return false ; } @Override public void onSwiped (@NonNull RecyclerView.ViewHolder viewHolder, int direction) { switch (direction) { case ItemTouchHelper.LEFT: adapterCallback.remove(viewHolder.getAdapterPosition()); } } }
自定义适配器并实现操作接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public class MainAdapter extends RecyclerView .Adapter <MainAdapter .MyViewHolder > implements MyCallback .AdapterCallback { private static final String TAG = "MainAdapter" ; private List<News> news; public MainAdapter (List<News> news) { this .news = news; } @NonNull @Override public MyViewHolder onCreateViewHolder (@NonNull ViewGroup parent, int viewType) { View itemView = LayoutInflater.from(parent.getContext()) .inflate(R.layout.recyclerview_news, parent, false ); return new MyViewHolder(itemView); } @Override public void onBindViewHolder (@NonNull MyViewHolder holder, int position) { holder.title.setText(news.get(position).title); holder.suttitle.setText(news.get(position).subtitle); holder.pic.setImageResource(R.mipmap.ic_launcher); } @Override public int getItemCount () { return news.size(); } @Override public void remove (int position) { news.remove(position); notifyItemRemoved(position); } static class MyViewHolder extends RecyclerView .ViewHolder { TextView title; TextView suttitle; ImageView pic; public MyViewHolder (@NonNull View itemView) { super (itemView); title = itemView.findViewById(R.id.news_title); suttitle = itemView.findViewById(R.id.news_subtitle); pic = itemView.findViewById(R.id.news_pic); } } }
MainActivity配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity" ; private RecyclerView recyclerView; private MainAdapter adapter; private SwipeRefreshLayout swipe; private List<News> news = listNews(); @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); recyclerView = findViewById(R.id.act_main_recyclerview); RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(this ); recyclerView.setLayoutManager(mLayoutManager); DefaultItemAnimator animator = new DefaultItemAnimator(); animator.setRemoveDuration(500 ); animator.setMoveDuration(500 ); recyclerView.setItemAnimator(animator); recyclerView.addItemDecoration(new DividerItemDecoration(this , LinearLayoutManager.VERTICAL)); adapter = new MainAdapter(news); recyclerView.setAdapter(adapter); ItemTouchHelper helper = new ItemTouchHelper(new MyCallback(adapter)); helper.attachToRecyclerView(recyclerView); swipe = findViewById(R.id.act_third_swipe); swipe.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { @Override public void onRefresh () { new Handler().postDelayed(() -> { swipe.setRefreshing(false ); news.add(0 , new News(1 , "阿根廷VS波黑" + news.size(), "小组赛F组 阿根廷VS波黑" )); adapter.notifyItemInserted(0 ); recyclerView.scrollToPosition(0 ); }, 500 ); } }); } private List<News> listNews () { List<News> news = new ArrayList<>(); News n1 = new News(1 , "阿根廷VS波黑" , "小组赛F组 阿根廷VS波黑" ); News n2 = new News(2 , "法国VS洪都拉斯" , "小组赛E组 法国VS洪都拉斯" ); News n3 = new News(3 , "瑞士VS厄瓜多尔" , "小组赛E组 瑞士VS厄瓜多尔" ); news.add(n1); news.add(n2); news.add(n3); return news; } }
复习完成 -> link
欲使用功能丰富的appbar/toolbar,需先关闭android自带的title/actionbar 自定义无title/actionbar样式的主题 在AndroidManifest配置中引入自定义主题 自定义独立的appbar布局文件,注意命名空间 在activity layout中引入(类似JSP的include) 在activity中获取toolbar对象,可动态修改各种属性 动态置于ActionBar,开启左箭头(可选)等等
自定义无title/actionbar样式的主题 : res/values/styles.xml 中
1 2 3 4 5 <style name ="AppTheme.NoActionBar" > <item name ="windowNoTitle" > true</item > <item name ="windowActionBar" > false</item > </style >
在AndroidManifest 配置中引入自定义主题:
android:theme="@style/AppTheme.NoActionBar">
自定义独立的appbar布局文件,注意命名空间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ---->> appbar.xml <?xml version="1.0" encoding="utf-8"?> <com.google.android.material.appbar.AppBarLayout xmlns:android ="http://schemas.android.com/apk/res/android" xmlns:app ="http://schemas.android.com/apk/res-auto" android:layout_width ="match_parent" android:layout_height ="wrap_content" > <androidx.appcompat.widget.Toolbar android:id ="@+id/my_toolbar" android:layout_width ="match_parent" android:layout_height ="?attr/actionBarSize" app:popupTheme ="@style/ThemeOverlay.AppCompat.Light" app:theme ="@style/ThemeOverlay.AppCompat.Dark.ActionBar" > </androidx.appcompat.widget.Toolbar > </com.google.android.material.appbar.AppBarLayout >
在activity layout中引入(类似JSP的include)
1 2 3 4 5 6 7 8 ---->> activity_main.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout ... tools:context =".MainActivity" > <include layout ="@layout/appbar" > </include > </LinearLayout >
在activity中获取toolbar对象,可动态修改各种属性 动态置于ActionBar,开启左箭头(可选)等等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); Toolbar toolbar = findViewById(R.id.my_toolbar); toolbar.setLogo(R.mipmap.ic_launcher); toolbar.setTitle("标题" ); setSupportActionBar(toolbar); getSupportActionBar().setDisplayHomeAsUpEnabled(true ); } ... ... }
创建menu资源目录; 创建menu布局文件; 设置item title,icon等属性: link
app:showAsAction属性: always, collapseActionView, ifRoom, never, withText
重写activity onCreateOptionsMenu()方法,加载menu布局 link
1 2 3 4 5 6 7 8 9 10 11 ... @Override public boolean onCreateOptionsMenu (Menu menu) { getMenuInflater().inflate(R.menu.menu, menu); return true ; }
重写activity onOptionsItemSelected()方法,监听item点击事件 link
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 ... @Override public boolean onOptionsItemSelected (MenuItem item) { String msg = "" ; switch (item.getItemId()) { case R.id.menu_add: msg = "add" ; break ; case android.R.id.home: msg = "home" ; finish(); case R.id.menu_send: msg = "send" ; break ; case R.id.menu_edit: msg = "edit" ; break ; case R.id.menu_del: msg = "delete" ; break ; } Toast.makeText(this , msg, Toast.LENGTH_SHORT).show(); return super .onOptionsItemSelected(item); } ...
Example 09 Navigation & BottomNavigationView 复习完成 -> link
Fragment & Navigation
在module gradle配置引入navigation-fragment,navigation-ui依赖 (不直接声明依赖,创建导航视图文件时也可自动引入,但AS会卡住假死) 创建多个Fragment及布局 创建navigation资源目录,创建nav_graph导航视图文件 在导航视图中引入fragment,声明导航规则 (也可设置fragment的独立global action) 修改activity_main布局,添加NavHostFragment容器,,引用导航视图,声明必须属性
引入依赖
1 2 3 def nav_version = "2.0.0" implementation "androidx.navigation:navigation-fragment:$nav_version" implementation "androidx.navigation:navigation-ui:$nav_version"
创建多个Fragment及布局
new -> fragment -> 勾选 Create Layout XML; 下面的2个include 选项不要勾选 (在创建 fragment 类的同时 创建布局文件)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class FoodFragment extends Fragment { private static final String TAG = "FoodFragment" ; @Override public View onCreateView (LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { return inflater.inflate(R.layout.fragment_food, container, false ); } @Override public void onViewCreated (@NonNull View view, @Nullable Bundle savedInstanceState) { super .onViewCreated(view, savedInstanceState); Button button = view.findViewById(R.id.frag_food_detail_button); button.setOnClickListener(v -> { Navigation.findNavController(v).navigate(R.id.action_foodFragment_to_foodDetailFragment); }); } }
创建navigation资源目录,创建nav_graph导航视图文件
在导航视图中引入fragment,声明导航规则 : 设置指定的id 导航到指定的 fragment类 和 它的布局
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android ="http://schemas.android.com/apk/res/android" xmlns:app ="http://schemas.android.com/apk/res-auto" xmlns:tools ="http://schemas.android.com/tools" android:id ="@+id/nav_graph" app:startDestination ="@id/foodFragment" > <fragment android:id ="@+id/foodFragment" android:name ="com.example.example09.FoodFragment" android:label ="fragment_food" tools:layout ="@layout/fragment_food" > <action android:id ="@+id/action_foodFragment_to_foodDetailFragment" app:destination ="@id/foodDetailFragment" /> </fragment > <fragment android:id ="@+id/foodDetailFragment" android:name ="com.example.example09.FoodDetailFragment" android:label ="fragment_food_detail" tools:layout ="@layout/fragment_food_detail" /> ... ... </navigation >
修改activity_main布局 ,添加NavHostFragment容器,,引用导航视图,声明必须属性
1 2 3 4 5 6 7 8 9 10 //activity_main <fragment android:id ="@+id/my_nav_host_fragment" android:name ="androidx.navigation.fragment.NavHostFragment" android:layout_width ="match_parent" android:layout_height ="0dp" android:layout_weight ="1" app:defaultNavHost ="true" app:navGraph ="@navigation/nav_graph" />
BottomNavigationView
com.google.android.material.bottomnavigation.BottomNavigationView (自动包含选中状态样式:图标加亮+显示字体,区别未选中其他item) 创建menu文件,声明Navigation所需item,图标及文字,绑定Navigation中的fragment 在activity布局声明引入底部导航BottomNavigationView控件,引用menu 在activity中获取NavController对象及并绑定BottomNavigationView对象
创建menu文件,声明Navigation所需item,图标及文字,绑定Navigation中的fragment
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ------->> menu_bottom_nav.xml <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android ="http://schemas.android.com/apk/res/android" > <item android:id ="@id/foodFragment" android:title ="Food" android:icon ="@drawable/food_u" /> <item android:id ="@id/hotelFragment" android:title ="Hotel" android:icon ="@drawable/hotel_u" /> <item android:id ="@id/mapFragment" android:title ="Map" android:icon ="@drawable/ic_loc_in_map_u" /> <item android:id ="@+id/menu_bottom_list" android:title ="List" android:icon ="@drawable/main_index_my_pressed" /> </menu >
在activity布局 声明引入底部导航BottomNavigationView控件,引用menu
1 2 3 4 5 <com.google.android.material.bottomnavigation.BottomNavigationView android:id ="@+id/bottom_nav" android:layout_width ="match_parent" android:layout_height ="wrap_content" app:menu ="@menu/menu_bottom_nav" />
在Activity类 中获取NavController对象及并绑定BottomNavigationView对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class MainActivity extends AppCompatActivity implements NavController .OnDestinationChangedListener { private static final String TAG = "MainActivity" ; private NavController controller; private BottomNavigationView bottomNavigationView; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); bottomNavigationView = findViewById(R.id.bottom_nav); controller = Navigation.findNavController(this , R.id.my_nav_host_fragment); controller.addOnDestinationChangedListener(this ); NavigationUI.setupWithNavController(bottomNavigationView, controller); } @Override public void onDestinationChanged (@NonNull NavController controller, @NonNull NavDestination destination, @Nullable Bundle arguments) { switch (destination.getId()) { case R.id.foodDetailFragment: bottomNavigationView.setVisibility(View.GONE); break ; default : bottomNavigationView.setVisibility(View.VISIBLE); } } }
Example 10 DrawerLayout & NavigationView 复习完成 -> link
基于DrawerLayout创建抽屉布局,从内向外逐层构建 引入com.google.android.material:material:1.0.0依赖 创建基本主内容布局,声明layout_behavior属性避免被appbar覆盖 可声明使用showIn属性,增加预览效果 创建appbar布局,声明toolbar,引入主内容布局(需声明使用noactionbar样式) 基于menu创建抽屉导航选项 基于DrawerLayout创建抽屉布局,引入appbar布局,NavigationView引用menu导航 可选创建抽屉头部布局,并引入到NavigationView属性(自定义了头部背景样式) Activity代码中添加actionbar 监听navigationView OnNavigationItemSelectedListener 任意item被点击,关闭抽屉 抽屉与下导航,选一种作为app主布局即可
引入依赖
1 implementation 'com.google.android.material:material:1.0.0'
创建基本主内容布局,声明layout_behavior属性避免被appbar覆盖
可声明使用showIn属性,增加预览效果 (预览该布局在外层layout中的效果)
1 2 3 4 5 6 7 8 9 10 11 ------->> content.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout ... app:layout_behavior ="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" tools:showIn ="@layout/appbar" > <TextView ... android:text ="左拉抽屉;单activity开发,用navfragmenthost替换" /> </LinearLayout >
创建appbar布局,声明toolbar,引入主内容布局 (需在AndroidManifest.xml和style.xml声明使用AppTheme.NoActionBar样式)<include layout = "">引入 content 布局
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 ------>> appbar.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout ... tools:showIn ="@layout/activity_main" > <com.google.android.material.appbar.AppBarLayout android:layout_width ="match_parent" android:layout_height ="wrap_content" > <androidx.appcompat.widget.Toolbar android:id ="@+id/my_toolbar" android:layout_width ="match_parent" android:layout_height ="?attr/actionBarSize" app:popupTheme ="@style/ThemeOverlay.AppCompat.Light" app:theme ="@style/ThemeOverlay.AppCompat.Dark.ActionBar" /> </com.google.android.material.appbar.AppBarLayout > <include layout ="@layout/content" /> </LinearLayout >
基于menu创建抽屉导航选项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <?xml version="1.0" encoding="utf-8"?> <menu xmlns:android ="http://schemas.android.com/apk/res/android" xmlns:tools ="http://schemas.android.com/tools" > <group android:checkableBehavior ="single" > <item android:id ="@+id/nav_camera" android:icon ="@android:drawable/ic_menu_camera" android:title ="camera" /> ... ... </group > <item android:title ="Communicate" > <menu > <item android:id ="@+id/nav_share" android:icon ="@android:drawable/ic_menu_share" android:title ="share" /> ... ... </menu > </item > </menu >
基于DrawerLayout创建抽屉布局,引入appbar布局,NavigationView引用menu导航
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 ---->> menu_drawer.xml <?xml version="1.0" encoding="utf-8"?> <androidx.drawerlayout.widget.DrawerLayout xmlns:android ="http://schemas.android.com/apk/res/android" xmlns:app ="http://schemas.android.com/apk/res-auto" xmlns:tools ="http://schemas.android.com/tools" android:id ="@+id/drawer_layout" android:layout_width ="match_parent" android:layout_height ="match_parent" android:fitsSystemWindows ="true" tools:context =".MainActivity" tools:openDrawer ="start" > <include layout ="@layout/appbar" android:layout_width ="match_parent" android:layout_height ="wrap_content" /> <com.google.android.material.navigation.NavigationView android:id ="@+id/nav_view" android:layout_width ="wrap_content" android:layout_height ="match_parent" android:layout_gravity ="start" android:fitsSystemWindows ="true" app:headerLayout ="@layout/drawer_header" app:menu ="@menu/menu_drawer" /> </androidx.drawerlayout.widget.DrawerLayout >
可选创建抽屉头部布局,并引入到NavigationView属性(自定义了头部背景样式)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 ---->> drawer_header.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android ="http://schemas.android.com/apk/res/android" xmlns:app ="http://schemas.android.com/apk/res-auto" android:layout_width ="match_parent" android:layout_height ="176dp" android:background ="@drawable/side_nav_bar" android:gravity ="bottom" android:orientation ="vertical" android:padding ="16dp" > <ImageView android:id ="@+id/imageView" android:layout_width ="wrap_content" android:layout_height ="wrap_content" android:paddingTop ="8dp" app:srcCompat ="@mipmap/ic_launcher_round" /> <TextView android:layout_width ="match_parent" android:layout_height ="wrap_content" android:layout_marginVertical ="8dp" android:textSize ="20sp" android:textStyle ="bold" android:textColor ="@color/colorAccent" android:text ="主标题" /> <TextView android:id ="@+id/textView" android:layout_width ="match_parent" android:layout_height ="wrap_content" android:text ="副标题" /> </LinearLayout >
Activity代码中添加actionbar 监听navigationView OnNavigationItemSelectedListener 任意item被点击,关闭抽屉
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public class MainActivity extends AppCompatActivity implements NavigationView .OnNavigationItemSelectedListener { private Toolbar toolbar; private DrawerLayout drawerLayout; private NavigationView navigationView; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); toolbar = findViewById(R.id.my_toolbar); drawerLayout = findViewById(R.id.drawer_layout); navigationView = findViewById(R.id.nav_view); setSupportActionBar(toolbar); getSupportActionBar().setDisplayHomeAsUpEnabled(true ); navigationView.setNavigationItemSelectedListener(this ); } @Override public boolean onNavigationItemSelected (@NonNull MenuItem menuItem) { switch (menuItem.getItemId()) { case R.id.nav_camera: Toast.makeText(this , "camera" , Toast.LENGTH_SHORT).show(); break ; } drawerLayout.closeDrawer(GravityCompat.START); return true ; } }
Example 11 SharedPreferences 接口 复习完成 -> link
Saving Key-Value Sets:https://developer.android.google.cn/training/data-storage/shared-preferences 基本的保存键值对数据的实现 自定义application,暴露获取application对象的静态方法 修改AndroidManifest配置启动自定义application 创建SharedPreferences操作工具类 编写输入输出布局 在activity中将输入写入文件 重新进入应用,文本输出控件,显式上次持久化的数据
SharedPreferences 需要上下文 使用: getApplicationContext() 拿
自定义application,暴露获取application对象的静态方法
1 2 3 4 5 6 7 8 9 10 11 public class MyApplication extends Application { private static MyApplication instance; public static MyApplication getInstance () { return instance; } @Override public void onCreate () { super .onCreate(); instance = this ; } }
修改AndroidManifest配置 启动自定义applicationandroid:name=".util.MyApplication"
创建SharedPreferences操作工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class SharedPreferencesUtils { private static SharedPreferences sf = create(); private static final String PRE_FILE = "pre_my" ; private static final String MYEDIT = "myedit" ; private static SharedPreferences create () { return MyApplication.getInstance() .getSharedPreferences(PRE_FILE, Context.MODE_PRIVATE); } public static void putMyedit (String myedit) { sf.edit().putString(MYEDIT, myedit).apply(); } public static String getMyedit () { return sf.getString(MYEDIT, "" ); } }
在activity中将输入写入文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); editText = findViewById(R.id.act_main_edittext); textView = findViewById(R.id.act_main_textView); button = findViewById(R.id.act_main_button); textView.setText(SharedPreferencesUtils.getMyedit()); button.setOnClickListener(v -> { SharedPreferencesUtils.putMyedit(editText.getEditableText().toString()); }); }
Example 12 DataBinding & ViewModel & LiveData 复习完成 -> link
在项目gradle配置中,启动dataBinding
1 2 3 dataBinding { enabled = true }
添加整合了viewmodel livedata的依赖lifecycle-extensions
1 2 3 def lifecycle_version = "2.0.0" // ViewModel and LiveData implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
官方推荐一个activity对应绑定一个ViewModel
基本实现,基于mainactivity 创建实体类
创建自定义viewmodel类
声明 页面数据绑定/生命周期绑定的MutableLiveData类型数据
创建修改方法,在子线程中修改数据
setValue() 在主线程 中更新数据
postValue() 在子线程 中更新数据 : 自动通知主线程修改
getValue()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public class MainViewModel extends AndroidViewModel { private static final String TAG = "MainViewModel" ; public MutableLiveData<User> userLiveData = new MutableLiveData<>(); public MainViewModel (@NonNull Application application) { super (application); User user = new User("BO" ); userLiveData.setValue(user); } public void change () { new Thread(() -> { try { Thread.sleep(2000 ); User u = userLiveData.getValue(); u.name = "SUN" ; userLiveData.postValue(u); } catch (InterruptedException e) { } }).start(); } }
修改layout文件,添加数据绑定标签<data><variable/></data>
绑定自定义的ViewModel类
在控件,通过表达式绑定数据,或方法
双向绑定 @={}
对象中的属性改变时,更新不会通知
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <?xml version="1.0" encoding="utf-8"?> <layout > <data > <variable name ="mianVM" type ="com.example.example12.viewmodel.MainViewModel" /> </data > <LinearLayout ... tools:context =".MainActivity" > <TextView ... android:text ="@{mianVM.userLiveData.name}" /> <Button android:onClick ="@{() -> mianVM.change()}" android:text ="异步改变值" /> <Button ... android:onClick ="onButtonClick" android:text ="To SecActivity" /> </LinearLayout > </layout >
修改activity代码,获取自定义动态创建的binding对象,获取自定义viewmodel对象
将vm绑定到UI页面
将绑定数据绑定到activity生命周期
Activity 的onCreate() 中基于layout文件生成绑定对象 (setContentView 删去)
绑定类会基于 layout 中生成的变量,自动生成 getter/setter 方法
绑定自定义的 ViewModel 类 :binding.setMianVM(mainViewModel);
将绑定数据,与当前activity生命周期绑定:binding.setMianVM(mainViewModel); 如,当数据改变时,且activity可见时,自动更新页面
activity有处理UI,跳转更新等操作,业务逻辑操作由vm负责
vm 不能绑定组件,应为可能已经被销毁了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity" ; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); ActivityMainBinding binding = DataBindingUtil.setContentView(this , R.layout.activity_main); MainViewModel mainViewModel = ViewModelProviders.of(this ).get(MainViewModel.class); binding.setMianVM(mainViewModel); binding.setLifecycleOwner(this ); } public void onButtonClick (View view) { Log.i(TAG, "onButtonClick: " ); Intent intent = new Intent(this , SecActivity.class); startActivity(intent); } }
整合recycleview的实现,基于secactivity 创建实体类 ,创建VM
创建更新可观测数据
初始化时,异步更新可观测数据
创建方法,异步更新可观测数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class SecViewModel extends AndroidViewModel { private static final String TAG = "SecViewModel" ; public MutableLiveData<List<News>> newsLoad = new MutableLiveData<>(); public SecViewModel (@NonNull Application application) { super (application); initNews(); } private void initNews () { new Thread(() -> { try { Thread.sleep(1000 ); News n1 = new News(1 , "阿根廷VS波黑" , "小组赛F组 阿根廷VS波黑" ); List<News> news = new ArrayList<>(); news.add(n1); newsLoad.postValue(news); } catch (InterruptedException e) { } }).start(); } public void loadNews () { new Thread(() -> { try { Thread.sleep(2000 ); News n1 = new News(1 , "荷兰VS西班牙" , "小组赛F组 荷兰VS西班牙" ); List<News> news = new ArrayList<>(); news.add(n1); newsLoad.postValue(news); } catch (InterruptedException e) { } }).start(); } }
创建recycleview_item布局 ,绑定实体类中属性
1 2 3 4 5 6 7 8 9 <?xml version="1.0" encoding="utf-8"?> <layout > <data > <variable name = "news" type ="com....News" /> </data > <LinearLayout > ... </LinearLayout > </layout >
修改layout ,绑定VM,绑定VM中更新方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <?xml version="1.0" encoding="utf-8"?> <layout > <data > <variable name ="secVM" type ="com.example.example12.viewmodel.SecViewModel" /> </data > <LinearLayout ... tools:context =".SecActivity" > <androidx.recyclerview.widget.RecyclerView android:id ="@+id/act_sec_recyclerview" /> <Button android:onClick ="@{() -> secVM.loadNews()}" android:text ="更新" /> <Button android:onClick ="toThird" android:text ="双向绑定" /> </LinearLayout > </layout >
创建自定义adapter ,初始化数据集合,重写基本方法
创建viewholder
viewholder不再holder控件,而是每一个itemview对应的binding对象 通过binding对象绑定集合中的数据
1 2 3 4 5 6 7 8 static class MyViewHolder extends RecyclerView .ViewHolder { private RecyclerviewNewsBinding binding; public MyViewHolder (@NonNull View itemView, RecyclerviewNewsBinding binding) { super (itemView); this .binding = binding; } }
重写onCreateViewHolder()方法,动态创建数据绑定对象
为每个item创建binding对象,复用
修改viewholder hold绑定对象
返回的 viewholder 基于绑定对象创建:return new MyViewHolder(binding.getRoot(), binding);
1 2 3 4 5 6 7 8 @NonNull @Override public MyViewHolder onCreateViewHolder (@NonNull ViewGroup parent, int viewType) { LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); RecyclerviewNewsBinding binding = DataBindingUtil.inflate(layoutInflater, R.layout.recyclerview_news, parent, false ); return new MyViewHolder(binding.getRoot(), binding); }
重写onBindViewHolder()方法,将当前viewholder的binding对象绑定对应的集合数据
binding 对象的set方法
1 2 3 4 @Override public void onBindViewHolder (@NonNull MyViewHolder holder, int position) { holder.binding.setNews(currentNewsList.get(position)); }
DiffUtil.Callback 自定义DiffUtil.Callback类,重写相关方法,实现更新adapter时的计算依据 adapter对外提供自己的更新方法 基于自定义Callback类,实现高效的,仅针对需更新项的,支持动画效果的,动态更新
Activity 修改activity代码,获取binding/viewmodel对象,绑定生命周期等 初始化recycleview,adapter等 监听自定义viewmodel中的数据更新,等有更新时,调用adapter提供的更新方法,通知其更新
1 2 3 4 5 6 7 8 9 10 11 12 13 ... @Override protected void onCreate (Bundle savedInstanceState) { ... viewModel.newsLoad.observe(this , news -> { Log.i(TAG, "onChanged" ); adapter.updateNews(news); recyclerView.scrollToPosition(0 ); }); ...
Two-way data binding 双向绑定 要比vue复杂。例如,封装在可观测数据内,数据的改变无法直接响应
双向绑定 @={}
Example 13 Connecting to the Network 复习完成 -> link
Network Request/Response & Image Resources 添加请求权限
1 2 3 在 AndroidManifest.xml 添加 <uses-permission android:name ="android.permission.INTERNET" /> <uses-permission android:name ="android.permission.ACCESS_NETWORK_STATE" />
创建自定义application类,配置
1 2 3 4 5 6 7 8 9 10 11 12 13 public class MyApplication extends Application { public static Application instance; @Override public void onCreate () { super .onCreate(); instance = this ; } public static Application getInstance () { return instance; } } ----------------------- AndroidManifest.xml 中 application 的 android:name=".util.MyApplication"
Android 9以后,要求网络请求必须为HTTPS加密请求,不便于测试,创建配置关闭该功能
1 2 3 4 5 6 <?xml version="1.0" encoding="utf-8"?> <network-security-config > <base-config cleartextTrafficPermitted ="true" /> </network-security-config > ------------------------ AndroidManifest.xml 中 application 的 android:networkSecurityConfig="@xml/network_security_config"
引入Retrofit框架依赖
1 2 implementation 'com.squareup.retrofit2:converter-gson:2.5.0' implementation 'com.squareup.retrofit2:retrofit:2.5.0'
创建实体类
创建封装响应数据的DTO类 (data transfer object)
1 2 3 4 5 6 7 8 9 10 public class NewsDTO { public News news; public List<News> newsList; }
创建网络请求接口,声明与后端对应的restful请求地址,图片资源请求处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public interface NewsService { @GET("news/{id}") Call<NewsDTO> getNews (@Path("id") int id) ; @GET("news") Call<NewsDTO> listNews () ; @GET Call<ResponseBody> getBitmap (@Url String url) ; @POST("news") Call<ResponseBody> post (@Body News n) ; }
构造封装retrofit对象,声明缓存,请求相对根路径,转换器,拦截器等配置
构造封装请求接口类型对象,并对外暴露
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class ServiceFactory { private static OkHttpClient client = new OkHttpClient.Builder() .cache(new Cache(MyApplication.getInstance().getCacheDir(), 10 * 1024 * 1024 )) .build(); private static Retrofit retrofit = new Retrofit.Builder() .baseUrl("http://www.whyman.site/api/" ) .client(client) .addConverterFactory(GsonConverterFactory.create()) .build(); public static NewsService getNewsService () { return retrofit.create(NewsService.class); } }
小小知识点: Builder 设计模式
在类中加一个静态内部类
静态内部类中有public方法设置属性
最后build() 方法,返回外部的类的对象
Builder模式通常作为配置类的构建器将配置的构建和表示分离开来,同时也是将配置从目标类中隔离出来,避免作为过多的setter方法,并且隐藏内部的细节。Builder模式比较常见的实现形式是通过链式调用,这样使得代码更加简洁、易懂。缺点是,内部类与外部类相互引用,可能会导致内存消耗比较大,不过鉴于现在的手机内存来讲,这点几乎影响不大。
编写布局文件 监听事件,调用retrofit完成异步的网络请求,并将结果渲染到视图
像网络请求这种线程阻塞的操作,禁止在主线程中执行
enqueue()为异步方法,将请求任务加入应用全局异步请求队列
在异步子线程中获取响应对象,在主线程,回调结果。即onResponse()方法为主线程调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity" ; private Button button; private TextView textView; private ImageView imageView; NewsService service = ServiceFactory.getNewsService(); @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); button = findViewById(R.id.button); textView = findViewById(R.id.textView); imageView = findViewById(R.id.imageView); button.setOnClickListener(v -> { service.listNews().enqueue(new Callback<NewsDTO>() { @Override public void onResponse (Call<NewsDTO> call, Response<NewsDTO> response) { if (response.body() == null ) { return ; } NewsDTO newsDTO = response.body(); List<News> newsList = newsDTO.newsList; textView.setText(newsList.get(0 ).title); } @Override public void onFailure (Call<NewsDTO> call, Throwable t) { } }); service.getBitmap("resources/pics/Spain_Flag.jpg" ).enqueue(new Callback<ResponseBody>() { @Override public void onResponse (Call<ResponseBody> call, Response<ResponseBody> response) { Log.i(TAG, "image" ); Bitmap bitmap = BitmapFactory.decodeStream( response.body().byteStream()); imageView.setImageBitmap(bitmap); } @Override public void onFailure (Call<ResponseBody> call, Throwable t) { } }); }); findViewById(R.id.act_main_button_tosec).setOnClickListener(v -> { Intent intent = new Intent(MainActivity.this , SecActivity.class); startActivity(intent); }); } }
Databinding & ViewModel & BindingAdapter 添加ViewModel LiveData依赖,声明启动Databinding
1 2 3 4 5 6 7 8 dataBinding { enabled = true } dependencies { def lifecycle_version = "2.0.0" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" }
创建Recyclerview item布局,绑定实体对象,自定义图片网络地址属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?xml version="1.0" encoding="utf-8"?> <layout > <data > <variable name ="news" type ="com.example.example13.entity.News" /> </data > <LinearLayout ... > <ImageView ... app:imageUrl ="@{news.picAddress}" ... /> ... </LinearLayout > </layout >
创建自定义图片绑定适配器,基于动态绑定的图片网络地址,下载图片并渲染
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class MyImageBindingAdapter { private static final String TAG = "MyImageBindingAdapter" ; @BindingAdapter({"imageUrl"}) public static void loadImage (ImageView view, String url) { if (url == null ) { view.setImageResource(R.mipmap.ic_launcher); return ; } ServiceFactory.getNewsService().getBitmap(url).enqueue(new Callback<ResponseBody>() { @Override public void onResponse (Call<ResponseBody> call, Response<ResponseBody> response) { if (response.body() == null ) { return ; } view.setImageBitmap(BitmapFactory.decodeStream(response.body().byteStream())); } @Override public void onFailure (Call<ResponseBody> call, Throwable t) { Log.e(TAG, "onFailure: " , t); } }); } }
创建ViewModel,声明绑定生命周期的可观测数据,新闻集合 创建加载方法,通过网络加载数据,并更新可观测数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class SecViewModel extends AndroidViewModel { public MutableLiveData<List<News>> newsList = new MutableLiveData<>(); private NewsService newsService = ServiceFactory.getNewsService(); public SecViewModel (@NonNull Application application) { super (application); } public void loadNews () { newsService.listNews().enqueue(new Callback<NewsDTO>() { @Override public void onResponse (Call<NewsDTO> call, Response<NewsDTO> response) { if (response.body() == null ) { return ; } List<News> news = response.body().newsList; newsList.setValue(news); } @Override public void onFailure (Call<NewsDTO> call, Throwable t) { } }); } }
修改layout布局,绑定VM,添加recycleview
创建recycleview的adapter,新闻集合属性,绑定item
修改activity代码 ,构造recycleview,绑定VM,绑定生命周期,监听网络返回的数据,通知adapter更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public class SecActivity extends AppCompatActivity { ... ... protected void onCreate (Bundle savedInstanceState) { ... viewModel.newsList.observe(this , news -> { adapter.setCurrentNewsList(news); adapter.notifyDataSetChanged(); recyclerView.scrollToPosition(0 ); }); ... } ... @Override protected void onStart () { super .onStart(); viewModel.loadNews(); } }
Post Request 在接口添加post请求
1 2 3 4 5 6 7 @POST("news") Call<ResponseBody> post (@Body News n) ;
创建第三个activity,实现2个输入框,1个button 实现当点击button时,调用post请求向服务器发送数据 重写SecActivity onStart()方法,调用VM中的网络数据加载方法,确保从暂停状态恢复,重新加载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class ThirdActivity extends AppCompatActivity { private static final String TAG = "ThirdActivity" ; private EditText title; private EditText subtitle; private Button submit; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_thrid); title = findViewById(R.id.act_third_edittext_title); subtitle = findViewById(R.id.act_third_edittext_subtitle); findViewById(R.id.act_third_button).setOnClickListener(v -> { News n = new News(); n.title = "" + title.getText().toString(); n.subtitle = "" + subtitle.getText().toString(); ServiceFactory.getNewsService().post(n).enqueue(new Callback<ResponseBody>() { @Override public void onResponse (Call<ResponseBody> call, Response<ResponseBody> response) { finish(); } @Override public void onFailure (Call<ResponseBody> call, Throwable t) { } }); }); } }
Example 14 Room Query SQLite数据库,SQLite操作无需系统权限
启动/引入数据绑定,VM,等 引用Room依赖
1 2 3 4 5 6 7 8 9 10 11 android { dataBinding { enabled = true } } dependencies { def lifecycle_version = "2.0.0" def room_version = "2.0.0" implementation "android.arch.lifecycle:extensions:$lifecycle_version" implementation "android.arch.persistence.room:runtime:$room_version" }
自定义引入application备用 (在AndroidManifest中引入)
创建实体类
1 2 3 4 5 6 7 @Entity public class Course { @PrimaryKey(autoGenerate = true) public int id; public String name; public String detail; }
创建操作实体类的DAO层接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Dao public interface CourseDao { @Query("SELECT * FROM Course") List<Course> list () ; @Query("SELECT * FROM Course c WHERE c.id=:id") Course find (int id) ; @Query("SELECT c.id, c.name FROM Course c") List<Course> listName () ; @Insert @Transaction void insert (Course... course) ; }
创建数据库工厂 ,封装构造过程,暴露DAO接口(代理类)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @Database(entities = {Course.class}, version = 1, exportSchema = false) public abstract class DatabaseFactory extends RoomDatabase { public abstract CourseDao courseDao () ; private static DatabaseFactory dataBaseFactory = Room .databaseBuilder(MyApplication.getInstance(), DatabaseFactory.class, "database") .allowMainThreadQueries() .build(); public static CourseDao getCourseDao () { return dataBaseFactory.courseDao(); } }
创建VM,从数据库获取数据,加载到observer对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class MainViewModel extends AndroidViewModel { private static final String TAG = "MainViewModel" ; public MutableLiveData<List<Course>> coursesM = new MutableLiveData<>(); public MainViewModel (@NonNull Application application) { super (application); } public void loadFromRoom () { List<Course> courses = DatabaseFactory.getCourseDao().listName(); coursesM.setValue(courses); } }
创建item layout,recyclerview adapter,完成基本绑定操作
修改activity代码,完成初始化,数据监听等操作
Insert & Snackbar 创建VM,声明绑定数据,声明添加方法,调用接口实现数据的插入
1 2 3 4 5 6 7 8 9 10 11 public class InsertCourseViewModel extends AndroidViewModel { public MutableLiveData<Course> courseM = new MutableLiveData<>(); public InsertCourseViewModel (@NonNull Application application) { super (application); courseM.setValue(new Course()); } public void insert () { DatabaseFactory.getCourseDao().insert(courseM.getValue()); } }
视图双向绑定VM数据,执行VM插入方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?xml version="1.0" encoding="utf-8"?> <layout > <data > <variable name ="insertVM" type ="com.example.example14.viewmodel.InsertCourseViewModel" /> </data > <LinearLayout ... > <EditText ... android:hint ="课程名称" android:text ="@={insertVM.courseM.name}" /> <EditText ... android:hint ="课程介绍" android:text ="@={insertVM.courseM.detail}" /> <Button ... android:text ="提交" android:onClick ="insert" /> </LinearLayout > </layout >
创建展示详细信息activity,修改adapter,绑定item点击监听,传递被点击item对应的数据ID参数跳转
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public class InsertCourseActivity extends AppCompatActivity { private InsertCourseViewModel vm; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); vm = ViewModelProviders.of(this ).get(InsertCourseViewModel.class); ActivityInsertCourseBinding binding = DataBindingUtil.setContentView(this , R.layout.activity_insert_course); binding.setInsertVM(vm); binding.setLifecycleOwner(this ); } public void insert (View view) { vm.insert(); Snackbar.make(view, "课程添加成功" , Snackbar.LENGTH_INDEFINITE) .setAction("确定" , v -> { finish(); }).show(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class MainRecyclerViewAdapter extends RecyclerView .Adapter <MainRecyclerViewAdapter .MyViewHolder > { private Context context; public MainRecyclerViewAdapter (Context context) { this .context = context; } ... @Override public void onBindViewHolder (@NonNull MyViewHolder holder, int position) { holder.binding.setCourse(courseList.get(position)); holder.itemView.setOnClickListener(v -> { Intent i = new Intent(context, CourseDetailActivity.class); i.putExtra("id" , courseList.get(position).id); context.startActivity(i); }); } ... }
展示详细信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 <layout> <data> <variable name="vm" type="com.example.example14.viewmodel.CourseDetailViewModel" /> </data> <LinearLayout ...> <TextView ... android:text="@{vm.courseM.name}" /> <TextView ... android:text="@{vm.courseM.detail}" /> </LinearLayout> </layout> public class CourseDetailViewModel extends AndroidViewModel { public MutableLiveData<Course> courseM = new MutableLiveData<>(); public CourseDetailViewModel (@NonNull Application application) { super (application); courseM.setValue(new Course()); } public void getCourse (int id) { Course c = DatabaseFactory.getCourseDao().find(id); courseM.setValue(c); } } public class CourseDetailActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); ActivityCourseDetailBinding binding = DataBindingUtil.setContentView(this , R.layout.activity_course_detail); CourseDetailViewModel vm = ViewModelProviders.of(this ).get(CourseDetailViewModel.class); binding.setVm(vm); binding.setLifecycleOwner(this ); vm.getCourse(getIntent().getIntExtra("id" , 0 )); } }
Snackbar的引入
1 implementation 'com.google.android.material:material:1.0.0'
Example 15 External Storage Internal Storage 内存储,其他应用无法访问的应用程序的独立空间,使用无需声明权限 文件随应用删除而删除,空间有限,放应用必须文件 getFilesDir() 拿files 中的文件夹 getCacheDir() 拿缓存中的文件夹
都是上下文的方法
/data/data/packagename/files
External Storage 外存储私有空间,无需声明权限,随应用卸载删除,放普通文件,缓存文件 挂载到,/mnt/sdcard/android/data/packname/files getExternalFilesDir()/getExternalCacheDir()
外存储公共空间,Android公共目录,音乐,图片等等,需声明权限 挂载到,/mnt/sdcard/ Environment.getExternalStoragePublicDirectory()
manifest 中: <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
Example 16 Dialogs 构造AlertDialog 自定义确认、取消、中性等按钮回调 单选项 单选项带确认按钮 自定义布局样式 多选项带确认按钮 DatePickerDialog,minsdk 24
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 public class MainActivity extends AppCompatActivity implements View .OnClickListener { private Button button; private Button button2; private Button button3; private Button button4; private Button button5; private Button button6; private Button button7; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); button = findViewById(R.id.button1); button2 = findViewById(R.id.button2); button3 = findViewById(R.id.button3); button4 = findViewById(R.id.button4); button5 = findViewById(R.id.button5); button6 = findViewById(R.id.button6); button7 = findViewById(R.id.button7); button.setOnClickListener(this ); button2.setOnClickListener(this ); button3.setOnClickListener(this ); button4.setOnClickListener(this ); button5.setOnClickListener(this ); button6.setOnClickListener(this ); button7.setOnClickListener(this ); } String[] arrayFruit = new String[]{"苹果" , "橘子" , "草莓" , "香蕉" }; boolean [] checkedItems = new boolean [arrayFruit.length]; int selectIndex = 0 ; @Override public void onClick (View v) { switch (v.getId()) { case R.id.button1: AlertDialog.Builder dialog = new AlertDialog.Builder(this ); dialog.setTitle("标题" ); dialog.setMessage("内容" ); dialog.setIcon(R.mipmap.ic_launcher); dialog.show(); break ; case R.id.button2: AlertDialog.Builder dialog2 = new AlertDialog.Builder(this ); dialog2.setTitle("删除" ); dialog2.setMessage("确定删除吗?" ); dialog2.setPositiveButton("确定" , new DialogInterface.OnClickListener() { @Override public void onClick (DialogInterface dialog, int which) { } }); dialog2.setNegativeButton("取消" , new DialogInterface.OnClickListener() { @Override public void onClick (DialogInterface dialog, int which) { dialog.dismiss(); } }); dialog2.setNeutralButton("详细内容" , new DialogInterface.OnClickListener() { @Override public void onClick (DialogInterface dialog, int which) { } }); dialog2.show(); break ; case R.id.button3: AlertDialog.Builder dialog3 = new AlertDialog.Builder(this ); dialog3.setTitle("水果" ); dialog3.setItems(arrayFruit, new DialogInterface.OnClickListener() { @Override public void onClick (DialogInterface dialog, int which) { Toast.makeText(MainActivity.this , arrayFruit[which], Toast.LENGTH_SHORT) .show(); } }); dialog3.show(); break ; case R.id.button4: AlertDialog.Builder dialog4 = new AlertDialog.Builder(this ); dialog4.setTitle("水果" ); dialog4.setSingleChoiceItems(arrayFruit, selectIndex, new DialogInterface .OnClickListener() { @Override public void onClick (DialogInterface dialog, int which) { selectIndex = which; } }); dialog4.setPositiveButton("确定" , new DialogInterface.OnClickListener() { @Override public void onClick (DialogInterface dialog, int which) { Toast.makeText(MainActivity.this , arrayFruit[selectIndex], Toast .LENGTH_SHORT).show(); } }); dialog4.show(); break ; case R.id.button5: AlertDialog.Builder dialog5 = new AlertDialog.Builder(this ); LayoutInflater inflater = LayoutInflater.from(this ); View rootView = inflater.inflate(R.layout.dialog_login, null ); dialog5.setTitle("登录" ); dialog5.setView(rootView); dialog5.show(); break ; case R.id.button6: AlertDialog.Builder dialog6 = new AlertDialog.Builder(this ); dialog6.setTitle("多选" ); dialog6.setIcon(R.mipmap.ic_launcher); dialog6.setMultiChoiceItems(arrayFruit, checkedItems, new DialogInterface .OnMultiChoiceClickListener() { @Override public void onClick (DialogInterface dialog, int which, boolean isChecked) { String string; if (isChecked) { string = "被选中了" ; } else { string = "被取消了" ; } Toast.makeText(MainActivity.this , arrayFruit[which] + string, Toast .LENGTH_SHORT).show(); } }); dialog6.setPositiveButton("确定" , new DialogInterface.OnClickListener() { @Override public void onClick (DialogInterface dialog, int which) { StringBuffer buffer = new StringBuffer(); for (int i = 0 ; i < arrayFruit.length; i++) { if (checkedItems[i]) { buffer.append(arrayFruit[i]); } } Toast.makeText(MainActivity.this , "被选中的水果: " + buffer.toString(), Toast .LENGTH_SHORT).show(); } }); dialog6.show(); break ; case R.id.button7: if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { DatePickerDialog dialog7 = new DatePickerDialog(this ); final Calendar calendar = Calendar.getInstance(); dialog7.setOnDateSetListener(new DatePickerDialog.OnDateSetListener() { @Override public void onDateSet (DatePicker view, int year, int month, int dayOfMonth) { calendar.set(Calendar.YEAR, year); calendar.set(Calendar.MONTH, month); calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth); String result = "日期: " + calendar.get(Calendar.YEAR) + "-" + calendar.get(Calendar.MONTH) + "-" + calendar.get(Calendar.DAY_OF_MONTH); Toast.makeText(MainActivity.this , result , Toast.LENGTH_SHORT).show(); } }); dialog7.show(); } break ; } } }
Example 17 Capture & Gallery 声明外存储写入权限<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
在配置中通过screenOrientation属性,将Activity强制设为横向或纵向屏幕,防止手机晃动后activity重置
1 <activity android:name =".MainActivity" android:screenOrientation ="portrait" > ...
在xml资源目录下,创建共享目录配置
1 2 3 4 5 6 7 res/xml/files_path <?xml version="1.0" encoding="utf-8"?> <paths > <external-path name ="public_path" path ="/" /> </paths >
在项目配置注册provider,并引用共享配置
1 2 3 4 5 6 7 8 9 10 11 AndroidManifest <provider android:name ="androidx.core.content.FileProvider" android:authorities ="com.example.example17" android:exported ="false" android:grantUriPermissions ="true" > <meta-data android:name ="android.support.FILE_PROVIDER_PATHS" android:resource ="@xml/file_paths" /> </provider >
在activity自定义照相请求码,写入权限码 在公共空间/DCIM/Camera/下,创建一个指定名称的图片文件,用于保存照片 创建拍照方法,基于FileProvider获取图片URI地址 启动需要结果的照相intent,封装请求吗及强制文件存储的URI地址 重写onActivityResult()方法,基于请求码判断返回的执行请求 未防止OOM,将图片按1/4渲染
Android6以后,要求运行时动态申请权限(类似iOS)https://developer.android.google.cn/training/permissions/requesting 重写onRequestPermissionsResult()方法,在用户授权后执行拍照方法 刷新相册(可选)
模拟器,可通过按住alt+鼠标移动镜头,wasd控制方向 Capture
Example 18 Notification https://developer.android.google.cn/training/notify-user/build-notification 构造Notification,声明必须属性 创建PendingIntent对象,封装点击通知intent 基于版本构造NotificationChannel 发送通知
Example 19 Service https://developer.android.google.cn/guide/components/services.html https://developer.android.google.cn/guide/components/bound-services.html https://developer.android.google.cn/training/run-background-service/create-service.html
自定义服务,封装操作值 重写onCreate()方法,创建子线程执行操作 自定义Binder子类,重写onBind()方法 重写onStartCommand()方法,接收初始化参数 修改项目配置,注册自定义服务
在activity中,自定义类实现ServiceConnection接口 基于自定义ServiceConnection服务连接类获取绑定对象 与服务互交,完成操作
Example 20 BroadcastReceiver https://developer.android.google.cn/reference/android/content/BroadcastReceiver.html
自定义接收器,重写onReceive()方法,监听指定action操作 自定义服务,在服务中注册/注销接收器,并声明监听的action 在项目配置中,注册服务,接收器 在activity中启动服务,监听action变化