使Forecast list可点击

作为一个真正的app,当前列表的每一个item布局应该做一些工作。第一件事就是创建一个合适的XML,能符合我们的需要就行。我们希望显示一个图标,日期,描述以及最高和最低温度。所以让我们创建一个名为item_forecast.xml的layout:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout
  3. xmlns:android="http://schemas.android.com/apk/res/android"
  4. xmlns:tools="http://schemas.android.com/tools"
  5. android:layout_width="match_parent"
  6. android:layout_height="match_parent"
  7. android:padding="@dimen/spacing_xlarge"
  8. android:background="?attr/selectableItemBackground"
  9. android:gravity="center_vertical"
  10. android:orientation="horizontal">
  11. <ImageView
  12. android:id="@+id/icon"
  13. android:layout_width="48dp"
  14. android:layout_height="48dp"
  15. tools:src="@mipmap/ic_launcher"/>
  16. <LinearLayout
  17. android:layout_width="0dp"
  18. android:layout_height="wrap_content"
  19. android:layout_weight="1"
  20. android:layout_marginLeft="@dimen/spacing_xlarge"
  21. android:layout_marginRight="@dimen/spacing_xlarge"
  22. android:orientation="vertical">
  23. <TextView
  24. android:id="@+id/date"
  25. android:layout_width="match_parent"
  26. android:layout_height="wrap_content"
  27. android:textAppearance="@style/TextAppearance.AppCompat.Medium"
  28. tools:text="May 14, 2015"/>
  29. <TextView
  30. android:id="@+id/description"
  31. android:layout_width="match_parent"
  32. android:layout_height="wrap_content"
  33. android:textAppearance="@style/TextAppearance.AppCompat.Caption"
  34. tools:text="Light Rain"/>
  35. </LinearLayout>
  36. <LinearLayout
  37. android:layout_width="wrap_content"
  38. android:layout_height="wrap_content"
  39. android:gravity="center_horizontal"
  40. android:orientation="vertical">
  41. <TextView
  42. android:id="@+id/maxTemperature"
  43. android:layout_width="wrap_content"
  44. android:layout_height="wrap_content"
  45. android:textAppearance="@style/TextAppearance.AppCompat.Medium"
  46. tools:text="30"/>
  47. <TextView
  48. android:id="@+id/minTemperature"
  49. android:layout_width="wrap_content"
  50. android:layout_height="wrap_content"
  51. android:textAppearance="@style/TextAppearance.AppCompat.Caption"
  52. tools:text="15"/>
  53. </LinearLayout>
  54. </LinearLayout>

Domain model和数据映射时必须生成完整的图标uil,所以我们可以这样去加载它:

  1. data class Forecast(val date: String, val description: String,
  2. val high: Int, val low: Int, val iconUrl: String)

ForecastDataMapper中:

  1. private fun convertForecastItemToDomain(forecast: Forecast): ModelForecast {
  2. return ModelForecast(convertDate(forecast.dt),
  3. forecast.weather[0].description, forecast.temp.max.toInt(),
  4. forecast.temp.min.toInt(), generateIconUrl(forecast.weather[0].icon))
  5. }
  6. private fun generateIconUrl(iconCode: String): String
  7. = "http://openweathermap.org/img/w/$iconCode.png"

我们从第一个请求中得到图标的code,用来组成完成的图标url。加载图片最简单的方式是使用图片加载库。Picasso是一个不错的选择。它需要加到build.gradle的依赖中:

  1. compile "com.squareup.picasso:picasso:<version>"

如此,Adapter也需要一个大的改动了。还需要一个click listener,我们来定义它:

  1. public interface OnItemClickListener {
  2. operator fun invoke(forecast: Forecast)
  3. }

如果你还记得上一课程,当被调用时invoke方法可以被省略。所以我们来使用它来简化。listener可以被以下两种方式调用:

  1. itemClick.invoke(forecast)
  2. itemClick(forecast)

ViewHolder将负责去绑定数据到新的View:

  1. class ViewHolder(view: View, val itemClick: OnItemClickListener) :
  2. RecyclerView.ViewHolder(view) {
  3. private val iconView: ImageView
  4. private val dateView: TextView
  5. private val descriptionView: TextView
  6. private val maxTemperatureView: TextView
  7. private val minTemperatureView: TextView
  8. init {
  9. iconView = view.find(R.id.icon)
  10. dateView = view.find(R.id.date)
  11. descriptionView = view.find(R.id.description)
  12. maxTemperatureView = view.find(R.id.maxTemperature)
  13. minTemperatureView = view.find(R.id.minTemperature)
  14. }
  15. fun bindForecast(forecast: Forecast) {
  16. with(forecast) {
  17. Picasso.with(itemView.ctx).load(iconUrl).into(iconView)
  18. dateView.text = date
  19. descriptionView.text = description
  20. maxTemperatureView.text = "${high.toString()}"
  21. minTemperatureView.text = "${low.toString()}"
  22. itemView.setOnClickListener { itemClick(forecast) }
  23. }
  24. }
  25. }

现在Adapter的构造方法接收一个itemClick。创建和绑定数据也是更简单:

  1. public class ForecastListAdapter(val weekForecast: ForecastList,
  2. val itemClick: ForecastListAdapter.OnItemClickListener) :
  3. RecyclerView.Adapter<ForecastListAdapter.ViewHolder>() {
  4. override fun onCreateViewHolder(parent: ViewGroup, viewType: Int):
  5. ViewHolder {
  6. val view = LayoutInflater.from(parent.ctx)
  7. .inflate(R.layout.item_forecast, parent, false)
  8. return ViewHolder(view, itemClick)
  9. }
  10. override fun onBindViewHolder(holder: ViewHolder, position: Int) {
  11. holder.bindForecast(weekForecast[position])
  12. }
  13. ...
  14. }

如果你使用了上面这些代码,parent.ctx不会被编译成功。Anko提供了大量的扩展函数来让Android编程更简单。举个例子,activitys、fragments以及其它包含了ctx这个属性,通过ctx这个属性来返回context,但是在View中缺少这个属性。所以我们要创建一个新的名叫ViewExtensions.kt文件来代替ui.utils,然后增加这个扩展属性:

  1. val View.ctx: Context
  2. get() = context

从现在开始,任何View都可以使用这个属性了。这个不是必须的,因为你可以使用扩展的context属性,但是我觉得如果我们使用ctx的话在其它类中也会更有连贯性。而且,这是一个很好的怎么去使用扩展属性的例子。

最后,MainActivity调用setAdapter,最后结果是这样的:

  1. forecastList.adapter = ForecastListAdapter(result,
  2. object : ForecastListAdapter.OnItemClickListener{
  3. override fun invoke(forecast: Forecast) {
  4. toast(forecast.date)
  5. }
  6. })

如你所见,创建一个匿名内部类,我们去创建了一个实现了刚刚创建的接口的对象。看起来不是很好,对吧?这是因为我们还没开始试使用另一个强大的函数式编程的特性,但是你将会在下一章中学习到怎么去把这些代码转换得更简单。

去代码库中更新新的代码。UI开始看起来更好了。