第14章 使用Kotlin 进行 Android 开发2
下面我们来介绍 ItemDetailFragment 。
ItemDetailFragment
这个 ItemDetailFragment 表示单个 Item 详细信息。此片段在双窗格模式 (在平板电脑上) 包含在 ItemListActivity 中,在手机上则是包含在ItemDetailActivity中。其 Kotlin 代码如下
package com.easy.kotlin import android.os.Bundle import android.support.v4.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import com.easy.kotlin.dummy.DummyContent import kotlinx.android.synthetic.main.activity_item_detail.* import kotlinx.android.synthetic.main.item_detail.view.* class ItemDetailFragment : Fragment() { /** * 测试数据 dummy content this fragment is presenting. */ private var mItem: DummyContent.DummyItem? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (arguments.containsKey(ARG_ITEM_ID)) { // 加载数据 mItem = DummyContent.ITEM_MAP[arguments.getString(ARG_ITEM_ID)] mItem?.let { // 给 toolbar_layout 布局设置标题 activity.toolbar_layout?.title = it.content } } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val rootView = inflater.inflate(R.layout.item_detail, container, false) // 在 TextView 中显示测试数据文本 mItem?.let { rootView.item_detail.text = it.details } return rootView } companion object { /** * The fragment argument representing the item ID that this fragment * represents. */ const val ARG_ITEM_ID = "item_id" } }
在 onCreate 中,activity.toolbar_layout?.title = it.content 这行代码是给详情页ToolBar 的大标题赋值
<android.support.design.widget.AppBarLayout android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="@dimen/app_bar_height" android:fitsSystemWindows="true" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> <android.support.design.widget.CollapsingToolbarLayout android:id="@+id/toolbar_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" app:contentScrim="?attr/colorPrimary" app:layout_scrollFlags="scroll|exitUntilCollapsed" app:toolbarId="@+id/toolbar"> <android.support.v7.widget.Toolbar android:id="@+id/detail_toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:layout_collapseMode="pin" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> </android.support.design.widget.CollapsingToolbarLayout> </android.support.design.widget.AppBarLayout>
对应的 UI 如下图
在 onCreateView中, rootView.item_detail.text = it.details 该行代码对应的布局是单个 Item 的详情展示 TextView 视图,其布局 XML 代码 item_detail.xml 如下
<TextView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/item_detail" style="?android:attr/textAppearanceLarge" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="16dp" android:textIsSelectable="true" tools:context="com.easy.kotlin.ItemDetailFragment" />
UI 效果图是
Fragment 生命周期
Fragment 必须嵌入在 Activity 中才能生存,其生命周期也直接受宿主 Activity 的生命周期的影响。比如,若宿主 Activity 处于 pause 状态,它所管辖的 Fragment 也将进入 pause 状态。而当 Activity 处于 resume 状态的时候,您可以独立地控制每一个 Fragment,如添加或删除等。为了创建Fragment,需要继承一个 Fragment 类,并实现 Fragment 的生命周期回调方法,如 onCreate(), onStart(), onPause(), onStop() 等。事实上,若需要在一个应用中加入 Fragment,只需要将原来的 Activity 替换为 Fragment,并将 Activity 的生命周期回调方法简单地改为 Fragment 的生命周期回调方法即可。Fragment 的生命周期如下所示:
另外, Fragment 与 Activity 的生命周期的对比图如下
1.当一个fragment被创建的时候,它会经历以下状态.
onAttach()
onCreate()
onCreateView()
onActivityCreated()
2.当这个fragment对用户可见的时候,它会经历以下状态
onStart()
onResume()
3.当这个fragment进入“后台模式”的时候,它会经历以下状态
onPause()
onStop()
4.当这个Fragment被销毁了(或者持有它的activity被销毁了),它会经历以下状态
onPause()
onStop()
onDestroyView()
onDetach()
5.就像 Activity 一样,在以下的状态中,可以使用Bundle对象保存一个Fragment的对象
onCreate()
onCreateView()
onActivityCreated()
6.Fragments的大部分状态都和 Activity 很相似,但 Fragment 有一些新的状态
onAttached() —— 当fragment和activity关联之后,调用这个方法。
onCreateView() —— 创建fragment中的视图的时候,调用这个方法。
onActivityCreated() —— 当activity的onCreate()方法被返回之后,调用这个方法。
onDestroyView() —— 当fragment中的视图被移除的时候,调用这个方法。
onDetach() —— 当fragment和activity分离的时候,调用这个方法。
一般来说,在 Fragment 中应至少重写下面3个生命周期方法:
- onCreate()
当创建 Fragment 实例时,系统回调的方法。在该方法中,需要对一些必要的组件进行初始化,以保证这个组件的实例在 Fragment 处于 pause或stop 状态时仍然存在。
- onCreateView()
当第一次在 Fragment 上绘制UI时,系统回调的方法。该方法返回一个 View 对象,该对象表示 Fragment 的根视图;若 Fragment 不需要展示视图,则该方法可以返回 null。
- onPause()
当用户离开 Fragment 时回调的方法(并不意味着该 Fragment 被销毁)。在该方法中,可以对 Fragment 的数据信息做一些持久化的保存工作,因为用户可能不再返回这个 Fragment。
大多数情况下,需要重写上述三个方法,有时还需要重写其他生命周期方法。
当执行一个 Fragment 事务时,也可以将该 Fragment 加入到一个由宿主 Activity 管辖的后退栈中,并由 Activity 记录加入到后退栈的 Fragment 信息,按下后退键可以将 Fragment 从后退栈中一次弹出。
将 Fragment 添加至 Activity 的视图布局中有两种方式:一种是使用fragment标签加入,Fragment的父视图应是一个ViewGroup;另一种使用代码动态加入,并将一个ViewGroup作为Fragment的容器。在某些情况下,Fragment并不作为Activity视图展示的一部分,它可能只是用来作为非显示性的功能。
Fragment 是 Android 3.0 (API level 11) 新加入的API,主要的设置目的是为了使UI在不同的屏幕上表现得更加灵活。由于平板比手机屏幕大的多,因此平板上可以呈现更多的内容,而 Fragment 可以实现同一视图布局在不同大小的屏幕上显示不同的效果,将 Fragment 加入到 Activity 的 Layout 中,可以在运行时动态替换 Fragment 并将 Fragment 保存至 Activity 管辖的“后退栈”中。另外,同样的界面Activity占用内存比Fragment要多,在中低端手机上Fragment 比 Activity 都响应速度要快很多。
为了方便起见,继承下面这些特殊的Fragment可以简化其初始化过程:
DialogFragment:可展示一个悬浮的对话框。使用该类创建的对话框可以很好地替换由 Activity 类中的方法创建的对话框,因为您可以像管理其他 Fragment 一样管理 DialogFragment——它们都被压入由宿主 Activity 管理的 Fragment 栈中,这可以很方便的找回已被压入栈中的 Fragment。
ListFragment:可以展示一个内置的 AdapterView,该 AdapterView 由一个 Adapter 管理着,如 SimpleCursorAdapter。ListFragment 类似于 ListActivity,它提供了大量的用于管理 ListView 的方法,比如回调方法 onListItemClick(),它用于处理点击项事件。
PreferenceFragment:可以展示层级嵌套的 Preference 对象列表。PreferenceFragment 类似于 PreferenceActivity,该类一般用于为应用程序编写设置页面。
Fragment 绑定 UI 布局必须重写 onCreateView() 方法,为 Fragment 绑定布局,该方法返回的 View 就是 Fragment 的根视图
class ItemDetailFragment : Fragment() { private var mItem: DummyContent.DummyItem? = null override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { val rootView = inflater.inflate(R.layout.item_detail, container, false) // Show the dummy content as text in a TextView. mItem?.let { rootView.item_detail.text = it.details } return rootView } }
其中,val rootView = inflater.inflate(R.layout.item_detail, container, false) 这一行代码中的 inflater.inflate 是用于填充布局的, 这是布局填充器 LayoutInflater 类的方法。通常我们加载布局的任务都是在 Activity 中调用 setContentView() 方法来完成的。其实 setContentView() 方法的内部也是使用LayoutInflater 来加载布局的,相关的代码在 android.support.v7.app.AppCompatDelegateImplV9 中
@Override public void setContentView(int resId) { ensureSubDecor(); ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content); contentParent.removeAllViews(); LayoutInflater.from(mContext).inflate(resId, contentParent); mOriginalWindowCallback.onContentChanged(); }
在实际开发中 LayoutInflater 这个类还是非常有用的,它的作用类似于findViewById()。不同点是LayoutInflater 是用来找 res/layout/ 下的 xml 布局文件并实例化(填充布局);而 findViewById() 是找 xml 布局文件下的具体widget控件(如Button、TextView等) 并实例化。
具体作用说明如下:
1、对于一个没有被载入或者想要动态载入的界面,都需要使用LayoutInflater.inflate()来载入;
2、对于一个已经载入的界面,就可以使用Activiyt.findViewById()方法来获得其中的界面元素。
注意:若继承的 Fragment 是 ListFragment,onCreateView() 方法已默认返回了 ListView 对象,故无需再重写该方法。
有关 Fragment 的更多信息,请参见“Fragment API指南” (http://developer.android.com/...)。
DummyContent
这个类中构造了我们在 ListActivity 中展示的测试数据。代码如下
package com.easy.kotlin.dummy import java.util.ArrayList import java.util.HashMap object DummyContent { val ITEMS: MutableList<DummyItem> = ArrayList() val ITEM_MAP: MutableMap<String, DummyItem> = HashMap() private val COUNT = 25 init { // Add some sample items. for (i in 1..COUNT) { addItem(createDummyItem(i)) } } private fun addItem(item: DummyItem) { ITEMS.add(item) ITEM_MAP.put(item.id, item) } private fun createDummyItem(position: Int): DummyItem { return DummyItem(position.toString(), "Item " + position, makeDetails(position)) } private fun makeDetails(position: Int): String { val builder = StringBuilder() builder.append("Details about Item: ").append(position) for (i in 0..position - 1) { builder.append("\nMore details information here.") } return builder.toString() } data class DummyItem(val id: String, val content: String, val details: String) { override fun toString(): String = content } }
至此,我们已经了解了怎样使用 Android Studio 3.0 创建一个带 ListActivity 和Fragment 的列表及其详情页展示,同时学习了 Activity 和 Fragment 的基本用法。下面我们来实现后端 API 的接入与数据的展现。
14.2.3 实现后端 API 的接入
在本节中我们将实现后端 API 的接入及其数据展示的逻辑。
新建领域对象类 Movie
data class Movie(val id: String, val title: String, val overview: String, val posterPath: String) { override fun toString(): String { return "Movie(id='$id', title='$title', overview='$overview', posterPath='$posterPath')" } }
API 返回的数据结构与解析
我们调用的 API 是
val VOTE_AVERAGE_API = "http://api.themoviedb.org//3/discover/movie?certification_country=US&certification=R&sort_by=vote_average.desc&api_key=7e55a88ece9f03408b895a96c1487979"
它的数据返回是
{ "page": 1, "total_results": 10350, "total_pages": 518, "results": [ { "vote_count": 28, "id": 138878, "video": false, "vote_average": 10, "title": "Fatal Mission", "popularity": 3.721883, "poster_path": "/u351Rsqu5nd36ZpbWxIpd3CpbJW.jpg", "original_language": "en", "original_title": "Fatal Mission", "genre_ids": [ 10752, 28, 12 ], "backdrop_path": "/wNq5uqVDT7a5G1b97ffYf4hxzYz.jpg", "adult": false, "overview": "A CIA Agent must rely on reluctant help from a female spy in the North Vietnam jungle in order to pass through enemy lines.", "release_date": "1990-07-25" }, ... ] }
我们使用 fastjson 来解析这个数据。在 app 下面的 build.gradle中添加依赖
dependencies { ... // https://mvnrepository.com/artifact/com.alibaba/fastjson compile group: 'com.alibaba', name: 'fastjson', version: '1.2.39' }
解析代码如下
val jsonstr = URL(VOTE_AVERAGE_API).readText(Charset.defaultCharset()) try { val obj = JSON.parse(jsonstr) as Map<*, *> val dataArray = obj.get("results") as JSONArray } } catch (ex: Exception) { }
然后我们把这个 dataArray 放到我们的 MovieContent 对象中
dataArray.forEachIndexed { index, it -> val title = (it as Map<*, *>).get("title") as String val overview = it.get("overview") as String val poster_path = it.get("poster_path") as String addMovie(Movie(index.toString(), title, overview, getPosterUrl(poster_path))) }
其中,addMovie 的代码是
object MovieContent { val MOVIES: MutableList<Movie> = ArrayList() val MOVIE_MAP: MutableMap<String, Movie> = HashMap() ... private fun addMovie(movie: Movie) { MOVIES.add(movie) MOVIE_MAP.put(movie.id, movie) } }
然后,我们再新建 MovieDetailActivity、MovieDetailFragment、MovieListActivity 以及 activity_movie_list.xml、activity_movie_detail.xml 、 movie_detail.xml、movie_list.xml、movie_list_content.xml ,它们的代码分别介绍如下。
电影列表页面
MovieListActivity 是电影列表页面的 Activity,代码如下
package com.easy.kotlin import android.content.Intent import android.os.Bundle import android.support.v7.app.AppCompatActivity import android.support.v7.widget.RecyclerView import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import com.easy.kotlin.bean.MovieContent import com.easy.kotlin.util.HttpUtil import kotlinx.android.synthetic.main.activity_movie_detail.* import kotlinx.android.synthetic.main.activity_movie_list.* import kotlinx.android.synthetic.main.movie_list.* import kotlinx.android.synthetic.main.movie_list_content.view.* class MovieListActivity : AppCompatActivity() { private var mTwoPane: Boolean = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_movie_list) setSupportActionBar(toolbar) toolbar.title = title if (movie_detail_container != null) { mTwoPane = true } setupRecyclerView(movie_list) } private fun setupRecyclerView(recyclerView: RecyclerView) { recyclerView.adapter = SimpleItemRecyclerViewAdapter(this, MovieContent.MOVIES, mTwoPane) } class SimpleItemRecyclerViewAdapter(private val mParentActivity: MovieListActivity, private val mValues: List<MovieContent.Movie>, private val mTwoPane: Boolean) : RecyclerView.Adapter<SimpleItemRecyclerViewAdapter.ViewHolder>() { private val mOnClickListener: View.OnClickListener init { mOnClickListener = View.OnClickListener { v -> val item = v.tag as MovieContent.Movie if (mTwoPane) { val fragment = MovieDetailFragment().apply { arguments = Bundle() arguments.putString(MovieDetailFragment.ARG_MOVIE_ID, item.id) } mParentActivity.supportFragmentManager .beginTransaction() .replace(R.id.movie_detail_container, fragment) .commit() } else { val intent = Intent(v.context, MovieDetailActivity::class.java).apply { putExtra(MovieDetailFragment.ARG_MOVIE_ID, item.id) } v.context.startActivity(intent) } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view = LayoutInflater .from(parent.context) .inflate(R.layout.movie_list_content, parent, false) return ViewHolder(view) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val item = mValues[position] holder.mIdView.text = item.id holder.mTitle.text = item.title holder.mMoviePosterImageView.setImageBitmap(HttpUtil.getBitmapFromURL(item.posterPath)) with(holder.itemView) { tag = item setOnClickListener(mOnClickListener) } } override fun getItemCount(): Int { return mValues.size } inner class ViewHolder(mView: View) : RecyclerView.ViewHolder(mView) { val mIdView: TextView = mView.id_text val mTitle: TextView = mView.title val mMoviePosterImageView: ImageView = mView.movie_poster_image } } }
对应的布局文件如下
activity_movie_list.xml
<?xml version="1.0" encoding="utf-8"?> <android.support.design.widget.CoordinatorLayout 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:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" tools:context="com.easy.kotlin.MovieListActivity"> <android.support.design.widget.AppBarLayout android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/AppTheme.AppBarOverlay"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:popupTheme="@style/AppTheme.PopupOverlay" /> </android.support.design.widget.AppBarLayout> <FrameLayout android:id="@+id/frameLayout" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior"> <include layout="@layout/movie_list" /> </FrameLayout> </android.support.design.widget.CoordinatorLayout>
movie_list.xml
<?xml version="1.0" encoding="utf-8"?> <android.support.v7.widget.RecyclerView 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/movie_list" android:name="com.easy.kotlin.MovieListFragment" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_marginLeft="16dp" android:layout_marginRight="16dp" app:layoutManager="LinearLayoutManager" tools:context="com.easy.kotlin.MovieListActivity" tools:listitem="@layout/movie_list_content" />
movie_list_content.xml
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="320dp" android:layout_gravity="center" android:layout_margin="0dp" android:clickable="true" android:foreground="?attr/selectableItemBackground" android:orientation="horizontal"> <TextView android:id="@+id/id_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="@dimen/text_margin" android:textAppearance="?attr/textAppearanceListItem" /> <ImageView android:id="@+id/movie_poster_image" android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerCrop" /> <View android:id="@+id/title_background" android:layout_width="match_parent" android:layout_height="48dp" android:layout_gravity="bottom" android:alpha="0.8" android:background="@color/colorPrimaryDark" android:gravity="center" /> <TextView android:id="@+id/title" android:layout_width="match_parent" android:layout_height="48dp" android:layout_gravity="bottom" android:gravity="center" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:textColor="@android:color/white" android:textSize="12sp" /> </FrameLayout>
电影列表的整体布局的 UI 如下图所示
视图数据适配器 ViewAdapter
我们在创建 MovieListActivity 过程中需要展示响应的数据,这些数据由 ViewAdapter 来承载,对应的代码如下
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_movie_list) setSupportActionBar(toolbar) toolbar.title = title if (movie_detail_container != null) { mTwoPane = true } setupRecyclerView(movie_list) } private fun setupRecyclerView(recyclerView: RecyclerView) { recyclerView.adapter = SimpleItemRecyclerViewAdapter(this, MovieContent.MOVIES, mTwoPane) }
在上面代码中,我们定义了一个继承 RecyclerView.Adapter 的 SimpleItemRecyclerViewAdapter 类来装载 View 中要显示的数据,实现数据与视图的解耦。View 要显示的数据从Adapter里面获取并展现出来。Adapter负责把真实的数据是配成一个个View,也就是说View要显示什么数据取决于Adapter里面的数据。
视图中图像的展示
其中,在函数 SimpleItemRecyclerViewAdapter.onBindViewHolder 中,我们设置 View 组件与Model 数据的绑定。其中的电影海报是图片,所以我们的布局文件中使用了 ImageView,对应的布局文件是 movie_list_content.xml ,代码如下
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="320dp" android:layout_gravity="center" android:layout_margin="0dp" android:clickable="true" android:foreground="?attr/selectableItemBackground" android:orientation="horizontal"> <TextView android:id="@+id/id_text" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="@dimen/text_margin" android:textAppearance="?attr/textAppearanceListItem" /> <ImageView android:id="@+id/movie_poster_image" android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerCrop" /> <View android:id="@+id/title_background" android:layout_width="match_parent" android:layout_height="48dp" android:layout_gravity="bottom" android:alpha="0.8" android:background="@color/colorPrimaryDark" android:gravity="center" /> <TextView android:id="@+id/title" android:layout_width="match_parent" android:layout_height="48dp" android:layout_gravity="bottom" android:gravity="center" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:textColor="@android:color/white" android:textSize="12sp" /> </FrameLayout>
UI 设计效果图
列表中图片的展示
关于图片的视图组件是 ImageView
<ImageView android:id="@+id/movie_poster_image" android:layout_width="match_parent" android:layout_height="match_parent" android:scaleType="centerCrop" />
我们这里是根据图片的 URL 来展示图片,ImageView 类有个setImageBitmap 方法,可以直接设置 Bitmap 图片数据
holder.mMoviePosterImageView.setImageBitmap(HttpUtil.getBitmapFromURL(item.posterPath))
而通过 url 获取Bitmap 图片数据的代码是
object HttpUtil { fun getBitmapFromURL(src: String): Bitmap? { try { val url = URL(src) val input = url.openStream() val myBitmap = BitmapFactory.decodeStream(input) return myBitmap } catch (e: Exception) { e.printStackTrace() return null } } }
电影详情页面
MovieDetailActivity 是电影详情页面,代码如下
package com.easy.kotlin import android.content.Intent import android.os.Bundle import android.support.design.widget.Snackbar import android.support.v7.app.AppCompatActivity import android.view.MenuItem import kotlinx.android.synthetic.main.activity_movie_detail.* class MovieDetailActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_movie_detail) setSupportActionBar(detail_toolbar) fab.setOnClickListener { view -> Snackbar.make(view, "Replace with your own detail action", Snackbar.LENGTH_LONG) .setAction("Action", null).show() } supportActionBar?.setDisplayHomeAsUpEnabled(true) if (savedInstanceState == null) { val arguments = Bundle() arguments.putString(MovieDetailFragment.ARG_MOVIE_ID, intent.getStringExtra(MovieDetailFragment.ARG_MOVIE_ID)) val fragment = MovieDetailFragment() fragment.arguments = arguments supportFragmentManager.beginTransaction() .add(R.id.movie_detail_container, fragment) .commit() } } override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) { android.R.id.home -> { navigateUpTo(Intent(this, MovieListActivity::class.java)) true } else -> super.onOptionsItemSelected(item) } }
其中的详情页的布局 XML 文件是activity_item_detail.xml, 代码如下
<android.support.design.widget.CoordinatorLayout 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:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" tools:context="com.easy.kotlin.ItemDetailActivity" tools:ignore="MergeRootFrame"> <android.support.design.widget.AppBarLayout android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="@dimen/app_bar_height" android:fitsSystemWindows="true" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> <android.support.design.widget.CollapsingToolbarLayout android:id="@+id/toolbar_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" app:contentScrim="?attr/colorPrimary" app:layout_scrollFlags="scroll|exitUntilCollapsed" app:toolbarId="@+id/toolbar"> <android.support.v7.widget.Toolbar android:id="@+id/detail_toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:layout_collapseMode="pin" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> </android.support.design.widget.CollapsingToolbarLayout> </android.support.design.widget.AppBarLayout> <android.support.v4.widget.NestedScrollView android:id="@+id/item_detail_container" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_behavior="@string/appbar_scrolling_view_behavior" /> <android.support.design.widget.FloatingActionButton android:id="@+id/fab" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_vertical|start" android:layout_margin="@dimen/fab_margin" app:layout_anchor="@+id/item_detail_container" app:layout_anchorGravity="top|end" app:srcCompat="@android:drawable/stat_notify_chat" /> </android.support.design.widget.CoordinatorLayout>
我们把电影详情的 Fragment 的展示放到 NestedScrollView 中
<android.support.v4.widget.NestedScrollView android:id="@+id/movie_detail_container" android:layout_width="match_parent" android:layout_height="match_parent" android:fitsSystemWindows="true" app:layout_behavior="@string/appbar_scrolling_view_behavior" />
电影详情的 Fragment 代码是 MovieDetailFragment
package com.easy.kotlin import android.os.Bundle import android.support.v4.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import com.easy.kotlin.bean.MovieContent import com.easy.kotlin.util.HttpUtil import kotlinx.android.synthetic.main.activity_movie_detail.* import kotlinx.android.synthetic.main.movie_detail.view.* class MovieDetailFragment : Fragment() { private var mItem: MovieContent.Movie? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (arguments.containsKey(ARG_MOVIE_ID)) { mItem = MovieContent.MOVIE_MAP[arguments.getString(ARG_MOVIE_ID)] mItem?.let { activity.toolbar_layout?.title = it.title } } } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { // 绑定 movieDetailView val movieDetailView = inflater.inflate(R.layout.movie_detail, container, false) mItem?.let { movieDetailView.movie_poster_image.setImageBitmap(HttpUtil.getBitmapFromURL(it.posterPath)) movieDetailView.movie_overview.text = "影片简介: ${it.overview}" movieDetailView.movie_vote_count.text = "打分次数:${it.vote_count}" movieDetailView.movie_vote_average.text = "评分:${it.vote_average}" movieDetailView.movie_release_date.text = "发行日期:${it.release_date}" } return movieDetailView } companion object { const val ARG_MOVIE_ID = "movie_id" } }
其中的 R.layout.movie_detail 布局文件 movie_detail.xml 如下
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" android:layout_margin="0dp" android:clickable="true" android:foreground="?attr/selectableItemBackground" android:orientation="vertical"> <TextView android:id="@+id/movie_release_date" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="16dp" android:textIsSelectable="true" android:textSize="18sp" tools:context="com.easy.kotlin.MovieDetailFragment" /> <ImageView android:id="@+id/movie_poster_image" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_centerVertical="true" android:fitsSystemWindows="true" android:scaleType="fitCenter" /> <TextView android:id="@+id/movie_overview" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="16dp" android:textIsSelectable="true" android:textSize="18sp" tools:context="com.easy.kotlin.MovieDetailFragment" /> <TextView android:id="@+id/movie_vote_average" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="16dp" android:textIsSelectable="true" android:textSize="18sp" tools:context="com.easy.kotlin.MovieDetailFragment" /> <TextView android:id="@+id/movie_vote_count" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="16dp" android:textIsSelectable="true" android:textSize="18sp" tools:context="com.easy.kotlin.MovieDetailFragment" /> </LinearLayout>
电影源数据的获取
我们定义了一个 MovieContent 对象类来存储从 API 获取到的数据,代码如下
package com.easy.kotlin.bean import android.os.StrictMode import com.alibaba.fastjson.JSON import com.alibaba.fastjson.JSONArray import java.net.URL import java.nio.charset.Charset import java.util.* object MovieContent { val MOVIES: MutableList<Movie> = ArrayList() val MOVIE_MAP: MutableMap<String, Movie> = HashMap() val VOTE_AVERAGE_API = "http://api.themoviedb.org//3/discover/movie?sort_by=popularity.desc&api_key=7e55a88ece9f03408b895a96c1487979&page=1" init { val policy = StrictMode.ThreadPolicy.Builder().permitAll().build() StrictMode.setThreadPolicy(policy) initMovieListData() } private fun initMovieListData() { val jsonstr = URL(VOTE_AVERAGE_API).readText(Charset.defaultCharset()) try { val obj = JSON.parse(jsonstr) as Map<*, *> val dataArray = obj.get("results") as JSONArray dataArray.forEachIndexed { index, it -> val title = (it as Map<*, *>).get("title") as String val overview = it.get("overview") as String val poster_path = it.get("poster_path") as String val vote_count = it.get("vote_count").toString() val vote_average = it.get("vote_average").toString() val release_date = it.get("release_date").toString() addMovie(Movie(id = index.toString(), title = title, overview = overview, vote_count = vote_count, vote_average = vote_average, release_date = release_date, posterPath = getPosterUrl(poster_path))) } } catch (ex: Exception) { ex.printStackTrace() } } private fun addMovie(movie: Movie) { MOVIES.add(movie) MOVIE_MAP.put(movie.id, movie) } fun getPosterUrl(posterPath: String): String { return "https://image.tmdb.org/t/p/w185_and_h278_bestv2$posterPath" } data class Movie(val id: String, val title: String, val overview: String, val vote_count: String, val vote_average: String, val release_date: String, val posterPath: String) }
在 Android 4.0 之后默认的线程模式是不允许在主线程中访问网络。为了演示效果,我们在访问网络的代码前,把 ThreadPolicy 设置为允许运行访问网络
val policy = StrictMode.ThreadPolicy.Builder().permitAll().build() StrictMode.setThreadPolicy(policy)
我们使用了一个 data class Movie 来存储电影对象数据
data class Movie(val id: String, val title: String, val overview: String, val vote_count: String, val vote_average: String, val release_date: String, val posterPath: String)
配置 AndroidManifest.xml
最后,我们配置 AndroidManifest.xml文件内容如下
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.easy.kotlin"> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> ... <activity android:name=".MovieListActivity" android:label="@string/app_name" android:theme="@style/AppTheme.NoActionBar"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".MovieDetailActivity" android:label="@string/title_movie_detail" android:parentActivityName=".MovieListActivity" android:theme="@style/AppTheme.NoActionBar"> <meta-data android:name="android.support.PARENT_ACTIVITY" android:value="com.easy.kotlin.MovieListActivity" /> </activity> </application> <uses-permission android:name="android.permission.INTERNET" /> </manifest>
因为我们要访问网络,所以需要添加该行配置
<uses-permission android:name="android.permission.INTERNET" />
再次打包安装运行,效果图如下
电影列表页面
点击进入电影详情页
本章小结
Android 中经常出现的空引用、API的冗余样板式代码等都是是驱动我们转向 Kotlin 语言的动力。另外,Kotlin 的 Android 视图 DSL Anko 可以我们从繁杂的 XML 视图配置文件中解放出来。我们可以像在 Java 中一样方便的使用 Android 开发的流行的库诸如 Butter Knife、Realm、RecyclerView等。当然,我们使用 Kotlin 集成这些库来进行 Andorid 开发,既能够直接使用我们之前的开发库,又能够从 Java 语言、Android API 的限制中出来。这不得不说是一件好事。