Netflix 2025 UI/UX modernization: colors, themes, card animations, Glide TV optimization
This commit is contained in:
@@ -63,6 +63,9 @@ dependencies {
|
|||||||
implementation 'androidx.tv:tv-material:1.0.0-alpha10'
|
implementation 'androidx.tv:tv-material:1.0.0-alpha10'
|
||||||
implementation 'com.google.android.material:material:1.11.0'
|
implementation 'com.google.android.material:material:1.11.0'
|
||||||
|
|
||||||
|
// LeakCanary for memory leak detection (TV-optimized with Toast notifications)
|
||||||
|
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'
|
||||||
|
|
||||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
|
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
|
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0'
|
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0'
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.example.tvmon
|
package com.example.tvmon
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.util.Log
|
||||||
import com.example.tvmon.di.appModules
|
import com.example.tvmon.di.appModules
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.android.ext.koin.androidLogger
|
import org.koin.android.ext.koin.androidLogger
|
||||||
@@ -8,14 +9,20 @@ import org.koin.core.context.startKoin
|
|||||||
import org.koin.core.logger.Level
|
import org.koin.core.logger.Level
|
||||||
|
|
||||||
class TvmonApplication : Application() {
|
class TvmonApplication : Application() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "TvmonApplication"
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
startKoin {
|
startKoin {
|
||||||
androidLogger(Level.ERROR)
|
androidLogger(Level.ERROR)
|
||||||
androidContext(this@TvmonApplication)
|
androidContext(this@TvmonApplication)
|
||||||
modules(appModules)
|
modules(appModules)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.d(TAG, "TvmonApplication initialized")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,17 +3,30 @@ package com.example.tvmon
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.bumptech.glide.GlideBuilder
|
import com.bumptech.glide.GlideBuilder
|
||||||
import com.bumptech.glide.annotation.GlideModule
|
import com.bumptech.glide.annotation.GlideModule
|
||||||
|
import com.bumptech.glide.load.DecodeFormat
|
||||||
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
||||||
import com.bumptech.glide.load.engine.cache.LruResourceCache
|
import com.bumptech.glide.load.engine.cache.LruResourceCache
|
||||||
import com.bumptech.glide.module.AppGlideModule
|
import com.bumptech.glide.module.AppGlideModule
|
||||||
|
import com.bumptech.glide.request.RequestOptions
|
||||||
|
|
||||||
@GlideModule
|
@GlideModule
|
||||||
class TvmonGlideModule : AppGlideModule() {
|
class TvmonGlideModule : AppGlideModule() {
|
||||||
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
||||||
val memoryCacheSizeBytes = 1024 * 1024 * 50
|
// TV-optimized memory cache: 30MB (lower than phone due to limited RAM)
|
||||||
|
val memoryCacheSizeBytes = 1024 * 1024 * 30
|
||||||
builder.setMemoryCache(LruResourceCache(memoryCacheSizeBytes.toLong()))
|
builder.setMemoryCache(LruResourceCache(memoryCacheSizeBytes.toLong()))
|
||||||
|
|
||||||
val diskCacheSizeBytes = 1024L * 1024L * 200L
|
// Disk cache: 150MB
|
||||||
|
val diskCacheSizeBytes = 1024L * 1024L * 150L
|
||||||
builder.setDiskCache(InternalCacheDiskCacheFactory(context, "image_cache", diskCacheSizeBytes))
|
builder.setDiskCache(InternalCacheDiskCacheFactory(context, "image_cache", diskCacheSizeBytes))
|
||||||
|
|
||||||
|
// Use RGB_565 for lower memory usage (good quality for TV)
|
||||||
|
builder.setDefaultRequestOptions(
|
||||||
|
RequestOptions()
|
||||||
|
.format(DecodeFormat.PREFER_RGB_565)
|
||||||
|
.disallowHardwareConfig() // Avoid hardware bitmaps that can cause issues on TV
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isManifestParsingEnabled(): Boolean = false
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ class DetailsFragment : DetailsSupportFragment() {
|
|||||||
Log.w(TAG, "setupDetails: Setting up details UI")
|
Log.w(TAG, "setupDetails: Setting up details UI")
|
||||||
val presenterSelector = ClassPresenterSelector()
|
val presenterSelector = ClassPresenterSelector()
|
||||||
val detailsPresenter = FullWidthDetailsOverviewRowPresenter(DetailsDescriptionPresenter())
|
val detailsPresenter = FullWidthDetailsOverviewRowPresenter(DetailsDescriptionPresenter())
|
||||||
detailsPresenter.actionsBackgroundColor = ContextCompat.getColor(requireContext(), R.color.primary)
|
detailsPresenter.actionsBackgroundColor = ContextCompat.getColor(requireContext(), R.color.accent)
|
||||||
detailsPresenter.backgroundColor = ContextCompat.getColor(requireContext(), R.color.detail_background)
|
detailsPresenter.backgroundColor = ContextCompat.getColor(requireContext(), R.color.detail_background)
|
||||||
presenterSelector.addClassPresenter(DetailsOverviewRow::class.java, detailsPresenter)
|
presenterSelector.addClassPresenter(DetailsOverviewRow::class.java, detailsPresenter)
|
||||||
presenterSelector.addClassPresenter(ListRow::class.java, ListRowPresenter())
|
presenterSelector.addClassPresenter(ListRow::class.java, ListRowPresenter())
|
||||||
|
|||||||
@@ -46,26 +46,26 @@ class MainFragment : BrowseSupportFragment(), OnItemViewClickedListener, OnItemV
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupUI() {
|
private fun setupUI() {
|
||||||
Log.w(TAG, "setupUI: Setting up UI")
|
Log.w(TAG, "setupUI: Setting up UI")
|
||||||
headersState = HEADERS_ENABLED
|
headersState = HEADERS_ENABLED
|
||||||
title = getString(R.string.browse_title)
|
title = getString(R.string.browse_title)
|
||||||
brandColor = ContextCompat.getColor(requireContext(), R.color.primary)
|
brandColor = ContextCompat.getColor(requireContext(), R.color.accent)
|
||||||
searchAffordanceColor = ContextCompat.getColor(requireContext(), R.color.search_opaque)
|
searchAffordanceColor = ContextCompat.getColor(requireContext(), R.color.accent)
|
||||||
|
|
||||||
adapter = rowsAdapter
|
adapter = rowsAdapter
|
||||||
onItemViewClickedListener = this
|
onItemViewClickedListener = this
|
||||||
onItemViewSelectedListener = this
|
onItemViewSelectedListener = this
|
||||||
|
|
||||||
setOnSearchClickedListener {
|
setOnSearchClickedListener {
|
||||||
val activity = requireActivity() as? MainActivity
|
val activity = requireActivity() as? MainActivity
|
||||||
activity?.showSearchDialog { query ->
|
activity?.showSearchDialog { query ->
|
||||||
val intent = Intent(requireContext(), com.example.tvmon.ui.search.SearchActivity::class.java).apply {
|
val intent = Intent(requireContext(), com.example.tvmon.ui.search.SearchActivity::class.java).apply {
|
||||||
putExtra("initial_query", query)
|
putExtra("initial_query", query)
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Log.w(TAG, "setupUI: UI setup complete, adapter set")
|
Log.w(TAG, "setupUI: UI setup complete, adapter set")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package com.example.tvmon.ui.presenter
|
|||||||
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.animation.OvershootInterpolator
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.leanback.widget.ImageCardView
|
import androidx.leanback.widget.ImageCardView
|
||||||
import androidx.leanback.widget.Presenter
|
import androidx.leanback.widget.Presenter
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
@@ -11,107 +13,150 @@ import com.example.tvmon.data.model.Category
|
|||||||
|
|
||||||
class ContentCardPresenter : Presenter() {
|
class ContentCardPresenter : Presenter() {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
|
companion object {
|
||||||
val cardView = ImageCardView(parent.context).apply {
|
private const val FOCUS_SCALE = 1.08f
|
||||||
isFocusable = true
|
private const val FOCUS_TRANSLATION_Z = 8f
|
||||||
isFocusableInTouchMode = true
|
private const val ANIMATION_DURATION = 150L
|
||||||
setBackgroundColor(parent.context.getColor(R.color.default_background))
|
|
||||||
}
|
}
|
||||||
return ViewHolder(cardView)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) {
|
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
|
||||||
val content = item as Content
|
val cardView = ImageCardView(parent.context).apply {
|
||||||
val cardView = viewHolder.view as ImageCardView
|
isFocusable = true
|
||||||
val res = cardView.context.resources
|
isFocusableInTouchMode = true
|
||||||
|
setBackgroundColor(ContextCompat.getColor(context, R.color.default_background))
|
||||||
cardView.titleText = content.title
|
setOnFocusChangeListener { view, hasFocus ->
|
||||||
cardView.contentText = null
|
animateFocus(view, hasFocus)
|
||||||
|
}
|
||||||
val width = res.getDimensionPixelSize(R.dimen.card_width)
|
}
|
||||||
val height = res.getDimensionPixelSize(R.dimen.card_height)
|
return ViewHolder(cardView)
|
||||||
cardView.setMainImageDimensions(width, height)
|
|
||||||
|
|
||||||
if (content.thumbnail.isNotBlank()) {
|
|
||||||
Glide.with(cardView.context)
|
|
||||||
.load(content.thumbnail)
|
|
||||||
.error(R.drawable.default_background)
|
|
||||||
.placeholder(R.drawable.default_background)
|
|
||||||
.centerCrop()
|
|
||||||
.into(cardView.mainImageView)
|
|
||||||
} else {
|
|
||||||
cardView.mainImageView.setImageResource(R.drawable.default_background)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUnbindViewHolder(viewHolder: ViewHolder) {
|
private fun animateFocus(view: View, hasFocus: Boolean) {
|
||||||
val cardView = viewHolder.view as ImageCardView
|
if (hasFocus) {
|
||||||
cardView.badgeImage = null
|
view.animate()
|
||||||
cardView.mainImage = null
|
.scaleX(FOCUS_SCALE)
|
||||||
}
|
.scaleY(FOCUS_SCALE)
|
||||||
|
.translationZ(FOCUS_TRANSLATION_Z)
|
||||||
|
.setDuration(ANIMATION_DURATION)
|
||||||
|
.setInterpolator(OvershootInterpolator(1.2f))
|
||||||
|
.start()
|
||||||
|
view.setBackgroundColor(ContextCompat.getColor(view.context, R.color.focused_bg))
|
||||||
|
} else {
|
||||||
|
view.animate()
|
||||||
|
.scaleX(1.0f)
|
||||||
|
.scaleY(1.0f)
|
||||||
|
.translationZ(0f)
|
||||||
|
.setDuration(ANIMATION_DURATION)
|
||||||
|
.setInterpolator(OvershootInterpolator(1.2f))
|
||||||
|
.start()
|
||||||
|
view.setBackgroundColor(ContextCompat.getColor(view.context, R.color.default_background))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) {
|
||||||
|
val content = item as Content
|
||||||
|
val cardView = viewHolder.view as ImageCardView
|
||||||
|
val res = cardView.context.resources
|
||||||
|
|
||||||
|
cardView.titleText = content.title
|
||||||
|
cardView.contentText = null
|
||||||
|
|
||||||
|
val width = res.getDimensionPixelSize(R.dimen.card_width)
|
||||||
|
val height = res.getDimensionPixelSize(R.dimen.card_height)
|
||||||
|
cardView.setMainImageDimensions(width, height)
|
||||||
|
|
||||||
|
if (content.thumbnail.isNotBlank()) {
|
||||||
|
Glide.with(cardView.context)
|
||||||
|
.load(content.thumbnail)
|
||||||
|
.error(R.drawable.default_background)
|
||||||
|
.placeholder(R.drawable.default_background)
|
||||||
|
.centerCrop()
|
||||||
|
.into(cardView.mainImageView)
|
||||||
|
} else {
|
||||||
|
cardView.mainImageView.setImageResource(R.drawable.default_background)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUnbindViewHolder(viewHolder: ViewHolder) {
|
||||||
|
val cardView = viewHolder.view as ImageCardView
|
||||||
|
cardView.badgeImage = null
|
||||||
|
cardView.mainImage = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SearchCardPresenter : Presenter() {
|
class SearchCardPresenter : Presenter() {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
|
companion object {
|
||||||
val cardView = ImageCardView(parent.context).apply {
|
private const val FOCUS_SCALE = 1.1f
|
||||||
isFocusable = true
|
private const val FOCUS_TRANSLATION_Z = 10f
|
||||||
isFocusableInTouchMode = true
|
private const val ANIMATION_DURATION = 100L
|
||||||
setBackgroundColor(parent.context.getColor(R.color.default_background))
|
|
||||||
cardType = ImageCardView.CARD_TYPE_INFO_UNDER
|
|
||||||
infoVisibility = View.VISIBLE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cardView.setOnFocusChangeListener { v, hasFocus ->
|
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
|
||||||
if (hasFocus) {
|
val cardView = ImageCardView(parent.context).apply {
|
||||||
v.animate()
|
isFocusable = true
|
||||||
.scaleX(1.1f)
|
isFocusableInTouchMode = true
|
||||||
.scaleY(1.1f)
|
setBackgroundColor(ContextCompat.getColor(context, R.color.default_background))
|
||||||
.translationZ(10f)
|
cardType = ImageCardView.CARD_TYPE_INFO_UNDER
|
||||||
.setDuration(100)
|
infoVisibility = View.VISIBLE
|
||||||
.start()
|
setOnFocusChangeListener { view, hasFocus ->
|
||||||
} else {
|
animateFocus(view, hasFocus)
|
||||||
v.animate()
|
}
|
||||||
.scaleX(1.0f)
|
}
|
||||||
.scaleY(1.0f)
|
return ViewHolder(cardView)
|
||||||
.translationZ(0f)
|
|
||||||
.setDuration(100)
|
|
||||||
.start()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ViewHolder(cardView)
|
private fun animateFocus(view: View, hasFocus: Boolean) {
|
||||||
}
|
if (hasFocus) {
|
||||||
|
view.animate()
|
||||||
override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) {
|
.scaleX(FOCUS_SCALE)
|
||||||
val content = item as Content
|
.scaleY(FOCUS_SCALE)
|
||||||
val cardView = viewHolder.view as ImageCardView
|
.translationZ(FOCUS_TRANSLATION_Z)
|
||||||
val res = cardView.context.resources
|
.setDuration(ANIMATION_DURATION)
|
||||||
|
.setInterpolator(OvershootInterpolator(1.2f))
|
||||||
cardView.titleText = content.title
|
.start()
|
||||||
cardView.contentText = content.category
|
view.setBackgroundColor(ContextCompat.getColor(view.context, R.color.focused_bg))
|
||||||
|
} else {
|
||||||
val width = res.getDimensionPixelSize(R.dimen.card_width)
|
view.animate()
|
||||||
val height = res.getDimensionPixelSize(R.dimen.card_height)
|
.scaleX(1.0f)
|
||||||
cardView.setMainImageDimensions(width, height)
|
.scaleY(1.0f)
|
||||||
|
.translationZ(0f)
|
||||||
if (content.thumbnail.isNotBlank()) {
|
.setDuration(ANIMATION_DURATION)
|
||||||
Glide.with(cardView.context)
|
.setInterpolator(OvershootInterpolator(1.2f))
|
||||||
.load(content.thumbnail)
|
.start()
|
||||||
.error(R.drawable.default_background)
|
view.setBackgroundColor(ContextCompat.getColor(view.context, R.color.default_background))
|
||||||
.placeholder(R.drawable.default_background)
|
}
|
||||||
.centerCrop()
|
|
||||||
.into(cardView.mainImageView)
|
|
||||||
} else {
|
|
||||||
cardView.mainImageView.setImageResource(R.drawable.default_background)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onUnbindViewHolder(viewHolder: ViewHolder) {
|
override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) {
|
||||||
val cardView = viewHolder.view as ImageCardView
|
val content = item as Content
|
||||||
cardView.badgeImage = null
|
val cardView = viewHolder.view as ImageCardView
|
||||||
cardView.mainImage = null
|
val res = cardView.context.resources
|
||||||
}
|
|
||||||
|
cardView.titleText = content.title
|
||||||
|
cardView.contentText = content.category
|
||||||
|
|
||||||
|
val width = res.getDimensionPixelSize(R.dimen.card_width)
|
||||||
|
val height = res.getDimensionPixelSize(R.dimen.card_height)
|
||||||
|
cardView.setMainImageDimensions(width, height)
|
||||||
|
|
||||||
|
if (content.thumbnail.isNotBlank()) {
|
||||||
|
Glide.with(cardView.context)
|
||||||
|
.load(content.thumbnail)
|
||||||
|
.error(R.drawable.default_background)
|
||||||
|
.placeholder(R.drawable.default_background)
|
||||||
|
.centerCrop()
|
||||||
|
.into(cardView.mainImageView)
|
||||||
|
} else {
|
||||||
|
cardView.mainImageView.setImageResource(R.drawable.default_background)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUnbindViewHolder(viewHolder: ViewHolder) {
|
||||||
|
val cardView = viewHolder.view as ImageCardView
|
||||||
|
cardView.badgeImage = null
|
||||||
|
cardView.mainImage = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CategoryCardPresenter : Presenter() {
|
class CategoryCardPresenter : Presenter() {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package com.example.tvmon.ui.presenter
|
package com.example.tvmon.ui.presenter
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.animation.OvershootInterpolator
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.leanback.widget.ImageCardView
|
import androidx.leanback.widget.ImageCardView
|
||||||
import androidx.leanback.widget.Presenter
|
import androidx.leanback.widget.Presenter
|
||||||
import com.example.tvmon.R
|
import com.example.tvmon.R
|
||||||
@@ -10,31 +13,62 @@ class EpisodePresenter(
|
|||||||
private val watchedEpisodeUrl: String? = null
|
private val watchedEpisodeUrl: String? = null
|
||||||
) : Presenter() {
|
) : Presenter() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val FOCUS_SCALE = 1.08f
|
||||||
|
private const val FOCUS_TRANSLATION_Z = 8f
|
||||||
|
private const val ANIMATION_DURATION = 150L
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {
|
||||||
val cardView = ImageCardView(parent.context).apply {
|
val cardView = ImageCardView(parent.context).apply {
|
||||||
isFocusable = true
|
isFocusable = true
|
||||||
isFocusableInTouchMode = true
|
isFocusableInTouchMode = true
|
||||||
setBackgroundColor(parent.context.getColor(R.color.default_background))
|
setBackgroundColor(ContextCompat.getColor(context, R.color.default_background))
|
||||||
|
setOnFocusChangeListener { view, hasFocus ->
|
||||||
|
animateFocus(view, hasFocus)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return ViewHolder(cardView)
|
return ViewHolder(cardView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun animateFocus(view: View, hasFocus: Boolean) {
|
||||||
|
if (hasFocus) {
|
||||||
|
view.animate()
|
||||||
|
.scaleX(FOCUS_SCALE)
|
||||||
|
.scaleY(FOCUS_SCALE)
|
||||||
|
.translationZ(FOCUS_TRANSLATION_Z)
|
||||||
|
.setDuration(ANIMATION_DURATION)
|
||||||
|
.setInterpolator(OvershootInterpolator(1.2f))
|
||||||
|
.start()
|
||||||
|
view.setBackgroundColor(ContextCompat.getColor(view.context, R.color.focused_bg))
|
||||||
|
} else {
|
||||||
|
view.animate()
|
||||||
|
.scaleX(1.0f)
|
||||||
|
.scaleY(1.0f)
|
||||||
|
.translationZ(0f)
|
||||||
|
.setDuration(ANIMATION_DURATION)
|
||||||
|
.setInterpolator(OvershootInterpolator(1.2f))
|
||||||
|
.start()
|
||||||
|
view.setBackgroundColor(ContextCompat.getColor(view.context, R.color.default_background))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) {
|
override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) {
|
||||||
val episode = item as Episode
|
val episode = item as Episode
|
||||||
val cardView = viewHolder.view as ImageCardView
|
val cardView = viewHolder.view as ImageCardView
|
||||||
val res = cardView.context.resources
|
val ctx = cardView.context
|
||||||
|
|
||||||
cardView.titleText = episode.number
|
cardView.titleText = episode.number
|
||||||
cardView.contentText = episode.title
|
cardView.contentText = episode.title
|
||||||
|
|
||||||
if (episode.url == watchedEpisodeUrl) {
|
if (episode.url == watchedEpisodeUrl) {
|
||||||
cardView.setBackgroundColor(res.getColor(R.color.accent))
|
cardView.setBackgroundColor(ContextCompat.getColor(ctx, R.color.accent))
|
||||||
} else {
|
} else {
|
||||||
cardView.setBackgroundColor(res.getColor(R.color.default_background))
|
cardView.setBackgroundColor(ContextCompat.getColor(ctx, R.color.default_background))
|
||||||
}
|
}
|
||||||
|
|
||||||
val width = res.getDimensionPixelSize(R.dimen.episode_card_width)
|
val width = ctx.resources.getDimensionPixelSize(R.dimen.episode_card_width)
|
||||||
val height = res.getDimensionPixelSize(R.dimen.episode_card_height)
|
val height = ctx.resources.getDimensionPixelSize(R.dimen.episode_card_height)
|
||||||
cardView.setMainImageDimensions(width, height)
|
cardView.setMainImageDimensions(width, height)
|
||||||
|
|
||||||
cardView.mainImageView.setImageResource(R.drawable.episode_placeholder)
|
cardView.mainImageView.setImageResource(R.drawable.episode_placeholder)
|
||||||
|
|||||||
@@ -1,10 +1,20 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<color name="primary">#1F1F1F</color>
|
<!-- Netflix 2025 Color Scheme -->
|
||||||
|
<color name="primary">#000000</color>
|
||||||
<color name="primary_dark">#000000</color>
|
<color name="primary_dark">#000000</color>
|
||||||
<color name="accent">#FF6B6B</color>
|
<color name="accent">#E50914</color>
|
||||||
<color name="default_background">#282828</color>
|
<color name="netflix_red">#E50914</color>
|
||||||
<color name="category_background">#3A3A3A</color>
|
<color name="netflix_black">#000000</color>
|
||||||
<color name="detail_background">#1F1F1F</color>
|
<color name="netflix_dark_gray">#141414</color>
|
||||||
|
<color name="netflix_medium_gray">#333333</color>
|
||||||
|
<color name="netflix_light_gray">#666666</color>
|
||||||
|
<color name="default_background">#141414</color>
|
||||||
|
<color name="category_background">#1F1F1F</color>
|
||||||
|
<color name="detail_background">#000000</color>
|
||||||
<color name="search_opaque">#AAFFFFFF</color>
|
<color name="search_opaque">#AAFFFFFF</color>
|
||||||
|
<color name="focused_card_border">#FFFFFF</color>
|
||||||
|
<color name="card_overlay">#B3000000</color>
|
||||||
|
<color name="focused_bg">#333333</color>
|
||||||
|
<color name="card_focused_bg">#2A2A2A</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<dimen name="card_width">180dp</dimen>
|
<!-- Netflix-style larger cards -->
|
||||||
<dimen name="card_height">240dp</dimen>
|
<dimen name="card_width">200dp</dimen>
|
||||||
|
<dimen name="card_height">280dp</dimen>
|
||||||
|
|
||||||
<dimen name="category_card_width">240dp</dimen>
|
<dimen name="category_card_width">240dp</dimen>
|
||||||
<dimen name="category_card_height">160dp</dimen>
|
<dimen name="category_card_height">160dp</dimen>
|
||||||
|
|
||||||
<dimen name="episode_card_width">200dp</dimen>
|
<dimen name="episode_card_width">220dp</dimen>
|
||||||
<dimen name="episode_card_height">120dp</dimen>
|
<dimen name="episode_card_height">130dp</dimen>
|
||||||
|
|
||||||
<dimen name="cast_card_width">120dp</dimen>
|
<dimen name="cast_card_width">120dp</dimen>
|
||||||
<dimen name="cast_card_height">160dp</dimen>
|
<dimen name="cast_card_height">160dp</dimen>
|
||||||
|
|
||||||
|
<!-- Netflix-style spacing -->
|
||||||
|
<dimen name="row_vertical_spacing">28dp</dimen>
|
||||||
|
<dimen name="card_horizontal_spacing">12dp</dimen>
|
||||||
|
<dimen name="screen_margin">48dp</dimen>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
<!-- Netflix 2025 Theme - Dark cinematic style -->
|
||||||
<style name="Theme.Tvmon" parent="Theme.Leanback">
|
<style name="Theme.Tvmon" parent="Theme.Leanback">
|
||||||
<item name="android:colorPrimary">@color/primary</item>
|
<item name="android:colorPrimary">@color/primary</item>
|
||||||
<item name="android:colorPrimaryDark">@color/primary_dark</item>
|
<item name="android:colorPrimaryDark">@color/primary_dark</item>
|
||||||
<item name="android:colorAccent">@color/accent</item>
|
<item name="android:colorAccent">@color/accent</item>
|
||||||
<item name="android:windowBackground">@color/default_background</item>
|
<item name="android:windowBackground">@color/netflix_black</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="Theme.Tvmon.Playback" parent="Theme.Leanback">
|
<style name="Theme.Tvmon.Playback" parent="Theme.Leanback">
|
||||||
@@ -17,6 +18,6 @@
|
|||||||
<item name="android:colorPrimary">@color/primary</item>
|
<item name="android:colorPrimary">@color/primary</item>
|
||||||
<item name="android:colorPrimaryDark">@color/primary_dark</item>
|
<item name="android:colorPrimaryDark">@color/primary_dark</item>
|
||||||
<item name="android:colorAccent">@color/accent</item>
|
<item name="android:colorAccent">@color/accent</item>
|
||||||
<item name="android:windowBackground">@color/default_background</item>
|
<item name="android:windowBackground">@color/netflix_black</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user