Skip to content

Commit

Permalink
Thumbnail image on item view (fixes #70) (#78)
Browse files Browse the repository at this point in the history
  • Loading branch information
stoyicker authored May 15, 2017
1 parent a25c122 commit fca69a7
Show file tree
Hide file tree
Showing 23 changed files with 158 additions and 117 deletions.
2 changes: 2 additions & 0 deletions app/lint.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
<ignore regexp="error" />
<ignore regexp="root" />
<ignore regexp="search" />
<ignore regexp="scroll_guide" />
<ignore regexp="thumbnail" />
</issue>
<issue id="MergeRootFrame">
<ignore regexp="include_top_posts_view" />
Expand Down
2 changes: 2 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@
-dontwarn android.test.**
-dontwarn kotlin.jvm.internal.Reflection
-dontwarn com.google.errorprone.annotations.*
-dontwarn com.squareup.okhttp.**

5 changes: 3 additions & 2 deletions app/src/androidTest/kotlin/app/AndroidTestApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ internal open class AndroidTestApplication : MainApplication() {
* @see app.gaming.TopGamingAllTimePostsActivity
*/
override fun buildTopGamingAllTimePostsFeatureComponent(
contentView: RecyclerView, errorView: View, progressView: View, activity: Activity) =
contentView: RecyclerView, errorView: View, progressView: View, guideView: View,
activity: Activity) =
DaggerTopGamingAllTimePostsFeatureInstrumentationComponent.builder()
.topGamingAllTimePostsFeatureInstrumentationModule(
TopGamingAllTimePostsFeatureInstrumentationModule(
contentView, errorView, progressView, activity))
contentView, errorView, progressView, guideView, activity))
.build()
}
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ internal class TopGamingActivityInstrumentation {
@Test
fun onLoadItemsAreShown() {
SUBJECT = ReplaySubject.create()
SUBJECT.onNext(Post("0", "Bananas title", "r/bananas", 879, "bananaLink"))
SUBJECT.onNext(Post("0", "Bananas title", "r/bananas", 879, "bananaLink", "tb"))
SUBJECT.onCompleted()
launchActivity()
onView(withId(R.id.progress)).check { view, _ ->
Expand Down Expand Up @@ -123,7 +123,7 @@ internal class TopGamingActivityInstrumentation {
@Test
fun onItemClickIntentIsFired() {
SUBJECT = ReplaySubject.create()
SUBJECT.onNext(Post("0", "Bananas title", "r/bananas", 879, "http://www.banan.as"))
SUBJECT.onNext(Post("0", "Bananas title", "r/bananas", 879, "http://www.banan.as", "tb"))
SUBJECT.onCompleted()
launchActivity()
Intents.init()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ internal class TopGamingAllTimePostsFeatureInstrumentationModule(
private val contentView: RecyclerView,
private val errorView: View,
private val progressView: View,
private val guideView: View,
private val activity: Activity) {
@Provides
@Singleton
Expand Down Expand Up @@ -84,7 +85,7 @@ internal class TopGamingAllTimePostsFeatureInstrumentationModule(
@Provides
@Singleton
fun topGamingAllTimePostsView() = TopGamingAllTimePostsView(
contentView, errorView, progressView)
contentView, errorView, progressView, guideView)

@Provides
@Singleton
Expand Down
5 changes: 3 additions & 2 deletions app/src/main/kotlin/app/MainApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ internal open class MainApplication : Application() {
* @see app.gaming.TopGamingAllTimePostsActivity
*/
internal open fun buildTopGamingAllTimePostsFeatureComponent(
contentView: RecyclerView, errorView: View, progressView: View, activity: Activity) =
contentView: RecyclerView, errorView: View, progressView: View, guideView: View,
activity: Activity) =
DaggerTopGamingAllTimePostsFeatureComponent.builder()
.topGamingAllTimePostsFeatureModule(
TopGamingAllTimePostsFeatureModule(
contentView, errorView, progressView, activity))
contentView, errorView, progressView, guideView, activity))
.build()
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ class TopGamingAllTimePostsActivity : AppCompatActivity() {
private fun inject() {
(application as MainApplication).buildTopGamingAllTimePostsFeatureComponent(
// https://kotlinlang.org/docs/tutorials/android-plugin.html#using-kotlin-android-extensions
content, error, progress, this).inject(this)
content, error, progress, scroll_guide, this).inject(this)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ internal class TopGamingAllTimePostsFeatureModule(
private val contentView: RecyclerView,
private val errorView: View,
private val progressView: View,
private val guideView: View,
private val activity: Activity) {
@Provides
@Singleton
Expand Down Expand Up @@ -93,7 +94,7 @@ internal class TopGamingAllTimePostsFeatureModule(
@Provides
@Singleton
fun topGamingAllTimePostsView() = TopGamingAllTimePostsView(
contentView, errorView, progressView)
contentView, errorView, progressView, guideView)

@Provides
@Singleton
Expand Down
95 changes: 76 additions & 19 deletions app/src/main/kotlin/app/gaming/TopGamingAllTimePostsFeatureView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import android.view.View
import android.view.ViewGroup
import android.widget.Filter
import android.widget.Filterable
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import com.squareup.picasso.Callback
import com.squareup.picasso.Picasso
import domain.entity.Post
import org.jorge.ms.app.R
import util.android.HtmlCompat
import util.android.ext.getDimension

/**
* Configuration for the recycler view holding the post list.
Expand Down Expand Up @@ -82,7 +83,7 @@ internal class Adapter(private val callback: TopGamingAllTimePostsActivity.Behav

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder
= ViewHolder(LayoutInflater.from(parent.context).inflate(
R.layout.item_post, parent, false), recyclerView, { callback.onItemClicked(it) })
R.layout.item_post, parent, false), { callback.onItemClicked(it) })

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.render(shownItems[position])
Expand All @@ -95,7 +96,7 @@ internal class Adapter(private val callback: TopGamingAllTimePostsActivity.Behav
return
}
// This is used to take the latest valid value in the given payload list
val combinator: (Bundle, String) -> Unit = { bundle, key ->
val folder: (Bundle, String) -> Unit = { bundle, key ->
@Suppress("UNCHECKED_CAST")
bundle.putString(key, (payloads as List<Bundle>).fold(Bundle(), { old, new ->
val oldTitle = old.getString(key)
Expand All @@ -104,8 +105,8 @@ internal class Adapter(private val callback: TopGamingAllTimePostsActivity.Behav
}).getString(key))
}
val combinedBundle = Bundle().also { bundle ->
arrayOf(KEY_TITLE, KEY_SUBREDDIT, KEY_SCORE).forEach {
combinator(bundle, it)
arrayOf(KEY_TITLE, KEY_SUBREDDIT, KEY_SCORE, KEY_THUMBNAIL).forEach {
folder(bundle, it)
}
}
// Now combinedBundle contains the latest version of each of the fields that can be updated
Expand Down Expand Up @@ -183,9 +184,10 @@ internal class Adapter(private val callback: TopGamingAllTimePostsActivity.Behav
}

override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int) =
shownItems[oldItemPosition].let { (_, oldTitle, oldSubreddit, oldScore) ->
shownItems[oldItemPosition].let {
(_, oldTitle, oldSubreddit, oldScore, oldThumbnail) ->
filteredItems[newItemPosition].let {
(_, newTitle, newSubreddit, newScore) ->
(_, newTitle, newSubreddit, newScore, newThumbnail) ->
Bundle().apply {
putString(KEY_TITLE, newTitle.takeIf {
!it.contentEquals(oldTitle)
Expand All @@ -196,6 +198,9 @@ internal class Adapter(private val callback: TopGamingAllTimePostsActivity.Behav
putString(KEY_SCORE, "${newScore.takeIf {
it != oldScore
}}")
putString(KEY_THUMBNAIL, newThumbnail.takeIf {
it != oldThumbnail
})
}
}
}
Expand All @@ -209,7 +214,6 @@ internal class Adapter(private val callback: TopGamingAllTimePostsActivity.Behav
override fun publishResults(constraint: CharSequence?, results: FilterResults?) {
@Suppress("UNCHECKED_CAST")
shownItems = results?.values as List<Post>? ?: items
(recyclerView.layoutParams as FrameLayout.LayoutParams).bottomMargin = 0
diff.dispatchUpdatesTo(this@Adapter)
}

Expand All @@ -222,23 +226,25 @@ internal class Adapter(private val callback: TopGamingAllTimePostsActivity.Behav
/**
* Very simple viewholder that sets text and click event handling.
* @param itemView The view to dump the held data.
* @param onItemClicked What to run when a click happens.
*/
internal class ViewHolder internal constructor(
itemView: View,
private val recyclerView: RecyclerView,
private val onItemClicked: (Post) -> Unit): RecyclerView.ViewHolder(itemView) {
private val titleView: TextView = itemView.findViewById(R.id.text_title) as TextView
private val scoreView: TextView = itemView.findViewById(R.id.text_score) as TextView
private val subredditView: TextView = itemView.findViewById(R.id.text_subreddit) as TextView
private val thumbnailView: ImageView = itemView.findViewById(R.id.thumbnail) as ImageView

/**
* Draw an item.
* @title The item to draw.
*/
internal fun render(item: Post) {
titleView.text = HtmlCompat.fromHtml(item.title)
subredditView.text = item.subreddit
scoreView.text = item.score.toString()
setTitle(item.title)
setSubreddit(item.subreddit)
setScore(item.score)
setThumbnail(item.thumbnailLink)
itemView.setOnClickListener { onItemClicked(item) }
}

Expand All @@ -248,26 +254,77 @@ internal class Adapter(private val callback: TopGamingAllTimePostsActivity.Behav
* @param item The item these updates correspond to.
*/
internal fun renderPartial(bundle: Bundle, item: Post) {
bundle.getString(KEY_TITLE).takeIf { it != null }.let { titleView.text =
HtmlCompat.fromHtml(it!!) }
bundle.getString(KEY_SUBREDDIT).takeIf { it != null }.let { subredditView.text = it }
bundle.getString(KEY_SCORE).takeIf { it != null }.let { scoreView.text = it }
bundle.getString(KEY_TITLE).takeIf { it != null }.let { setTitle(it!!) }
bundle.getString(KEY_SUBREDDIT).takeIf { it != null }.let { setSubreddit(it!!) }
bundle.getString(KEY_SCORE).takeIf { it != null }.let {
setScore(Integer.valueOf(it!!))
}
setThumbnail(bundle.getString(KEY_THUMBNAIL))
itemView.setOnClickListener { onItemClicked(item) }
}

/**
* Adds a margin under the recycler view for the progress and error views to show.
*/
internal fun addBottomMargin() {
(recyclerView.layoutParams as FrameLayout.LayoutParams).bottomMargin =
itemView.context.getDimension(R.dimen.footer_padding).toInt()
}

/**
* Updates the layout according to the changes required by a new title.
* @param title The new title.
*/
private fun setTitle(title: String) {
val formattedTitle = HtmlCompat.fromHtml(title)
titleView.text = formattedTitle
thumbnailView.contentDescription = formattedTitle.toString()
}

/**
* Updates the layout according to the changes required by a new subreddit.
* @param name The new subreddit name.
*/
private fun setSubreddit(name: String) {
subredditView.text = name
}

/**
* Updates the layout according to the changes required by a new score.
* @param score The new score.
*/
private fun setScore(score: Int) {
scoreView.text = score.toString()
}

/**
* Updates the layout according to the changes required by a new thumbnail link.
* @param thumbnailLink The new thumbnail link, or <code>null</code> if none is applicable.
*/
private fun setThumbnail(thumbnailLink: String?) {
if (thumbnailLink != null) {
Picasso.with(thumbnailView.context)
.load(thumbnailLink)
.into(thumbnailView, object : Callback {
override fun onError() {
thumbnailView.visibility = View.GONE
thumbnailView.setImageDrawable(null)
}

override fun onSuccess() {
thumbnailView.visibility = View.VISIBLE
}
})
} else {
thumbnailView.visibility = View.GONE
thumbnailView.setImageDrawable(null)
}
}
}

private companion object {
private val KEY_TITLE = "KEY_TITLE"
private val KEY_SUBREDDIT = "KEY_SUBREDDIT"
private val KEY_SCORE = "KEY_SCORE"
private val KEY_THUMBNAIL = "KEY_THUMBNAIL"
}
}

Expand Down
16 changes: 15 additions & 1 deletion app/src/main/kotlin/app/gaming/TopGamingAllTimePostsView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,24 @@ package app.gaming

import android.support.v7.widget.RecyclerView
import android.view.View
import android.widget.FrameLayout
import app.LoadableContentView
import domain.entity.Post
import org.jorge.ms.app.R
import util.android.ext.getDimension

/**
* Wraps UI behavior for top all time gaming posts scenario.
*/
internal class TopGamingAllTimePostsView(
internal val contentView: RecyclerView,
internal val errorView: View,
private val progressView: View) : LoadableContentView<Post> {
private val progressView: View,
private val guideView: View) : LoadableContentView<Post> {
override fun showLoadingLayout() {
pushInfoArea()
progressView.visibility = View.VISIBLE
guideView.visibility = View.INVISIBLE
}

override fun hideLoadingLayout() {
Expand All @@ -22,13 +28,21 @@ internal class TopGamingAllTimePostsView(

override fun updateContent(actionResult: List<Post>) {
(contentView.adapter as Adapter).addItems(actionResult)
guideView.visibility = View.VISIBLE
}

override fun showErrorLayout() {
pushInfoArea()
errorView.visibility = View.VISIBLE
guideView.visibility = View.INVISIBLE
}

override fun hideErrorLayout() {
errorView.visibility = View.GONE
}

private fun pushInfoArea() {
(contentView.layoutParams as FrameLayout.LayoutParams).bottomMargin =
contentView.context.getDimension(R.dimen.footer_padding).toInt()
}
}
10 changes: 9 additions & 1 deletion app/src/main/res/layout-v21/item_post.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,28 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:padding="@dimen/appbar_horizontal_padding"
android:theme="@style/AppTheme.Card">
<TextView
android:id="@+id/text_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/section_separator"
android:textIsSelectable="false"
android:clickable="false"
style="@style/AppTheme.TextTitle" />
<ImageView
android:id="@+id/thumbnail"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:visibility="gone"
tools:ignore="ContentDescription"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="@dimen/section_separator"
android:layout_marginBottom="@dimen/section_separator"
android:clickable="false"
android:background="@color/windowBackground" />
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/res/layout/include_top_posts_view.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,14 @@
android:textStyle="bold"
android:text="@string/top_posts_error_action" />
</LinearLayout>
<TextView
android:id="@+id/scroll_guide"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal"
android:gravity="center"
android:textStyle="bold"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
android:text="@string/top_posts_scroll_guide"
android:visibility="invisible" />
</FrameLayout>
Loading

0 comments on commit fca69a7

Please sign in to comment.