Skip to content

Commit 01a900a

Browse files
authored
Version 0.1.4
2 parents 5c8455b + 0646511 commit 01a900a

8 files changed

Lines changed: 255 additions & 90 deletions

File tree

content-view/content-view-core/src/main/java/com/devgary/contentview/AbstractContentHandlerView.kt

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.devgary.contentview
22

33
import android.content.Context
4+
import android.os.Parcel
5+
import android.os.Parcelable
46
import android.util.AttributeSet
57
import android.util.Log
68
import android.widget.FrameLayout
@@ -19,6 +21,7 @@ abstract class AbstractContentHandlerView @JvmOverloads constructor(
1921
) : FrameLayout(context, attrs, defStyleAttr), ContentHandler, Disposable, PlayPausable {
2022

2123
private val contentHandlers = mutableListOf<ContentHandler>()
24+
private var lastUsedHandler: ContentHandler? = null
2225

2326
init {
2427
registerContentHandlers()
@@ -48,11 +51,14 @@ abstract class AbstractContentHandlerView @JvmOverloads constructor(
4851
val firstContentHandlerForContent = contentHandlers.firstOrNull { handler ->
4952
handler.canShowContent(content)
5053
}
54+
55+
lastUsedHandler = null
5156
setContentHandlersVisibility(GONE, excludedContentHandler = firstContentHandlerForContent)
5257

5358
firstContentHandlerForContent?.let { handler ->
5459
addContentHandlerViewIfNotAdded(handler)
5560
handler.showContent(content)
61+
lastUsedHandler = handler
5662
} ?: run {
5763
Log.e(TAG, "No ${name<ContentHandler>()} found for ${classNameWithValue(content)}")
5864
}
@@ -92,4 +98,61 @@ abstract class AbstractContentHandlerView @JvmOverloads constructor(
9298
(it as? PlayPausable)?.setAutoplay(autoplay)
9399
}
94100
}
101+
102+
override fun onSaveInstanceState(): Parcelable? {
103+
val state = super.onSaveInstanceState()
104+
105+
state?.let {
106+
val savedState = SavedState(it)
107+
savedState.positionOfLastUsedContentHandler = contentHandlers.indexOf(lastUsedHandler)
108+
return savedState
109+
}
110+
111+
return state
112+
}
113+
114+
override fun onRestoreInstanceState(state: Parcelable?) {
115+
if (state is SavedState) {
116+
super.onRestoreInstanceState(state.superState)
117+
contentHandlers.getOrNull(state.positionOfLastUsedContentHandler)?.let {
118+
// In onRestoreInstanceState(), immediately add back the view that was
119+
// being used to display content. Otherwise, since the ContentHandlerViews
120+
// are lazily created, there is a risk that when onRestoreInstanceState()
121+
// is propagated throughout the views, it is missed by the ContentHandlerView
122+
// since it has not been created back yet and its state is not restored
123+
addContentHandlerViewIfNotAdded(it)
124+
}
125+
} else {
126+
super.onRestoreInstanceState(state)
127+
}
128+
}
129+
130+
internal class SavedState : BaseSavedState {
131+
// TODO: Use better way of tracking which ContentHandler used than position
132+
var positionOfLastUsedContentHandler = -1
133+
134+
constructor(source: Parcel) : super(source) {
135+
positionOfLastUsedContentHandler = source.readInt()
136+
}
137+
138+
constructor(superState: Parcelable) : super(superState)
139+
140+
override fun writeToParcel(out: Parcel, flags: Int) {
141+
super.writeToParcel(out, flags)
142+
out.writeInt(positionOfLastUsedContentHandler)
143+
}
144+
145+
companion object {
146+
@JvmField
147+
val CREATOR = object : Parcelable.Creator<SavedState> {
148+
override fun createFromParcel(source: Parcel): SavedState {
149+
return SavedState(source)
150+
}
151+
152+
override fun newArray(size: Int): Array<SavedState?> {
153+
return arrayOfNulls(size)
154+
}
155+
}
156+
}
157+
}
95158
}

demo/build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,9 @@ dependencies {
6767
implementation "androidx.appcompat:appcompat:$versions.appcompat"
6868
implementation "androidx.core:core-ktx:$versions.androidKtx"
6969
implementation "com.google.android.material:material:$versions.material"
70-
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
70+
implementation "androidx.constraintlayout:constraintlayout:$versions.constraintlayout"
7171
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$versions.lifecycle"
72+
implementation "androidx.navigation:navigation-fragment-ktx:$versions.navigation"
7273

7374
testImplementation "junit:junit:$versions.junit4"
7475
androidTestImplementation "androidx.test.ext:junit:$versions.junitAndroidExt"
Lines changed: 1 addition & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,16 @@
11
package com.devgary.contentviewdemo
22

33
import android.os.Bundle
4-
import android.view.Menu
5-
import android.view.MenuItem
6-
import android.view.View
7-
import android.widget.Toast
84
import androidx.appcompat.app.AppCompatActivity
9-
import androidx.lifecycle.lifecycleScope
10-
import com.devgary.testcore.SampleContent
11-
import com.devgary.contentlinkapi.handlers.gfycat.GfycatContentLinkHandler
12-
import com.devgary.contentlinkapi.handlers.imgur.ImgurContentLinkHandler
13-
import com.devgary.contentlinkapi.handlers.streamable.StreamableContentLinkHandler
14-
import com.devgary.contentlinkapi.content.BaseContentLinkHandler
15-
import com.devgary.contentlinkapi.content.CompositeContentLinkHandler
16-
import com.devgary.contentlinkapi.content.ContentLinkHandler
175
import com.devgary.contentviewdemo.databinding.ActivityDemoBinding
18-
import com.devgary.contentviewdemo.util.cancel
19-
import kotlinx.coroutines.CoroutineExceptionHandler
20-
import kotlinx.coroutines.Job
21-
import kotlinx.coroutines.launch
226

237
class DemoActivity : AppCompatActivity() {
248
private lateinit var binding: ActivityDemoBinding
25-
26-
private val contentLinkHandler: CompositeContentLinkHandler by lazy {
27-
object : BaseContentLinkHandler() {
28-
override fun provideContentHandlers(): List<ContentLinkHandler> {
29-
return listOf(
30-
GfycatContentLinkHandler(
31-
clientId = BuildConfig.GFYCAT_CLIENT_ID,
32-
clientSecret = BuildConfig.GFYCAT_CLIENT_SECRET
33-
),
34-
ImgurContentLinkHandler(
35-
authorizationHeader = BuildConfig.IMGUR_AUTHORIZATION_HEADER,
36-
mashapeKey = BuildConfig.IMGUR_MASHAPE_KEY
37-
),
38-
StreamableContentLinkHandler(),
39-
DemoFallthroughContentLinkHandler()
40-
)
41-
}
42-
}
43-
}
44-
45-
var getContentJob: Job? = null
46-
private val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
47-
Toast.makeText(
48-
/* context = */ this,
49-
/* text = */ "Error: ${throwable.message}",
50-
/* duration = */ Toast.LENGTH_LONG
51-
).show()
52-
}
53-
9+
5410
override fun onCreate(savedInstanceState: Bundle?) {
5511
super.onCreate(savedInstanceState)
5612
binding = ActivityDemoBinding.inflate(layoutInflater)
5713
setContentView(binding.root)
5814
setSupportActionBar(binding.toolbar)
59-
60-
showContent(SampleContent.IMAGE_CONTENT)
61-
}
62-
63-
override fun onCreateOptionsMenu(menu: Menu): Boolean {
64-
menuInflater.inflate(R.menu.menu_demo, menu)
65-
return true
66-
}
67-
68-
override fun onOptionsItemSelected(item: MenuItem): Boolean {
69-
when(item.itemId) {
70-
R.id.menu_image -> SampleContent.IMAGE_CONTENT
71-
R.id.menu_image_no_ext -> SampleContent.IMAGE_CONTENT_NO_EXTENSION
72-
R.id.menu_gif -> SampleContent.GIF_CONTENT
73-
R.id.menu_video -> SampleContent.MP4_VIDEO_CONTENT
74-
R.id.menu_streamable -> SampleContent.STREAMABLE.BASIC_URL
75-
R.id.menu_streamable_parse_webpage -> SampleContent.STREAMABLE.HLS_URL
76-
R.id.menu_gfycat_video -> SampleContent.GFYCAT_URL
77-
R.id.menu_imgur_album -> SampleContent.IMGUR_ALBUM_GALLERY_URL
78-
R.id.menu_clear_memory -> {
79-
contentLinkHandler.clearMemory()
80-
return true
81-
}
82-
else -> return super.onOptionsItemSelected(item)
83-
}.let { url ->
84-
showContent(url)
85-
return true
86-
}
87-
}
88-
89-
private fun showContent(url: String) {
90-
getContentJob.cancel()
91-
binding.contentview.setViewVisibility(View.GONE)
92-
getContentJob = lifecycleScope.launch(coroutineExceptionHandler) {
93-
contentLinkHandler.getContent(url)?.let { content ->
94-
binding.contentview.showContent(content)
95-
}
96-
}
9715
}
9816
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package com.devgary.contentviewdemo
2+
3+
import androidx.lifecycle.*
4+
import com.devgary.contentcore.model.content.Content
5+
import com.devgary.contentlinkapi.content.BaseContentLinkHandler
6+
import com.devgary.contentlinkapi.content.CompositeContentLinkHandler
7+
import com.devgary.contentlinkapi.content.ContentLinkHandler
8+
import com.devgary.contentlinkapi.handlers.gfycat.GfycatContentLinkHandler
9+
import com.devgary.contentlinkapi.handlers.imgur.ImgurContentLinkHandler
10+
import com.devgary.contentlinkapi.handlers.streamable.StreamableContentLinkHandler
11+
import com.devgary.contentviewdemo.util.cancel
12+
import com.devgary.testcore.SampleContent
13+
import kotlinx.coroutines.CoroutineExceptionHandler
14+
import kotlinx.coroutines.Job
15+
import kotlinx.coroutines.launch
16+
17+
class DemoViewModel : ViewModel() {
18+
private val contentLinkHandler: CompositeContentLinkHandler by lazy {
19+
object : BaseContentLinkHandler() {
20+
override fun provideContentHandlers(): List<ContentLinkHandler> {
21+
return listOf(
22+
GfycatContentLinkHandler(
23+
clientId = BuildConfig.GFYCAT_CLIENT_ID,
24+
clientSecret = BuildConfig.GFYCAT_CLIENT_SECRET
25+
),
26+
ImgurContentLinkHandler(
27+
authorizationHeader = BuildConfig.IMGUR_AUTHORIZATION_HEADER,
28+
mashapeKey = BuildConfig.IMGUR_MASHAPE_KEY
29+
),
30+
StreamableContentLinkHandler(),
31+
DemoFallthroughContentLinkHandler()
32+
)
33+
}
34+
}
35+
}
36+
37+
private var getContentJob: Job? = null
38+
private val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
39+
_error.postValue(throwable.message)
40+
}
41+
42+
private val _content = MutableLiveData<Content>()
43+
val content: LiveData<Content> = _content
44+
45+
private val _error = MutableLiveData<String>()
46+
val error: LiveData<String> = _error
47+
48+
init {
49+
// TODO: Remove test code
50+
if (content.value == null) {
51+
loadContent(SampleContent.IMAGE_CONTENT)
52+
}
53+
}
54+
55+
fun loadContent(url: String) {
56+
getContentJob.cancel()
57+
getContentJob = viewModelScope.launch(coroutineExceptionHandler) {
58+
contentLinkHandler.getContent(url)?.let { it ->
59+
_content.postValue(it)
60+
}
61+
}
62+
}
63+
64+
fun clearMemory() {
65+
contentLinkHandler.clearMemory()
66+
}
67+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package com.devgary.contentviewdemo.util
2+
import android.os.Bundle
3+
import android.util.Log
4+
import android.view.*
5+
import android.widget.Toast
6+
import androidx.core.view.MenuHost
7+
import androidx.core.view.MenuProvider
8+
import androidx.fragment.app.Fragment
9+
import androidx.lifecycle.Lifecycle
10+
import androidx.lifecycle.ViewModelProvider
11+
import com.devgary.contentcore.util.TAG
12+
import com.devgary.contentcore.util.name
13+
import com.devgary.contentviewdemo.DemoViewModel
14+
import com.devgary.contentviewdemo.R
15+
import com.devgary.contentviewdemo.databinding.FragmentDemoBinding
16+
import com.devgary.testcore.SampleContent
17+
18+
class DemoFragment : Fragment(), MenuProvider {
19+
private val demoViewModel: DemoViewModel by lazy {
20+
ViewModelProvider(this).get(DemoViewModel::class.java)
21+
}
22+
23+
private var _binding: FragmentDemoBinding? = null
24+
25+
/**
26+
* This property is only valid between [onCreateView] and [onDestroyView]
27+
*/
28+
private val binding get() = _binding!!
29+
30+
override fun onCreateView(
31+
inflater: LayoutInflater,
32+
container: ViewGroup?,
33+
savedInstanceState: Bundle?
34+
): View {
35+
_binding = FragmentDemoBinding.inflate(inflater, container, false)
36+
setHasOptionsMenu(true)
37+
return binding.root
38+
}
39+
40+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
41+
super.onViewCreated(view, savedInstanceState)
42+
43+
initMenu()
44+
initViewModel()
45+
}
46+
47+
private fun initMenu() {
48+
(requireActivity() as? MenuHost)?.let {
49+
it.addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED)
50+
} ?: run {
51+
Log.e(TAG, "Could not cast activity to ${name<MenuHost>()}")
52+
}
53+
}
54+
55+
override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
56+
menuInflater.inflate(R.menu.menu_demo, menu)
57+
}
58+
59+
override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
60+
when(menuItem.itemId) {
61+
R.id.menu_image -> SampleContent.IMAGE_CONTENT
62+
R.id.menu_image_no_ext -> SampleContent.IMAGE_CONTENT_NO_EXTENSION
63+
R.id.menu_gif -> SampleContent.GIF_CONTENT
64+
R.id.menu_video -> SampleContent.MP4_VIDEO_CONTENT
65+
R.id.menu_streamable -> SampleContent.STREAMABLE.BASIC_URL
66+
R.id.menu_streamable_parse_webpage -> SampleContent.STREAMABLE.HLS_URL
67+
R.id.menu_gfycat_video -> SampleContent.GFYCAT_URL
68+
R.id.menu_imgur_album -> SampleContent.IMGUR_ALBUM_GALLERY_URL
69+
R.id.menu_clear_memory -> {
70+
demoViewModel.clearMemory()
71+
return true
72+
}
73+
else -> return false
74+
}.let { url ->
75+
demoViewModel.loadContent(url)
76+
return true
77+
}
78+
}
79+
80+
private fun initViewModel() {
81+
demoViewModel.content.observe(viewLifecycleOwner) {
82+
binding.contentview.showContent(it)
83+
}
84+
85+
demoViewModel.error.observe(viewLifecycleOwner) {
86+
Toast.makeText(
87+
/* context = */ context,
88+
/* text = */ "Error: $it",
89+
/* duration = */ Toast.LENGTH_LONG
90+
).show()
91+
}
92+
}
93+
94+
override fun onDestroyView() {
95+
super.onDestroyView()
96+
_binding = null
97+
}
98+
}

demo/src/main/res/layout/activity_demo.xml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@
2020
/>
2121

2222
</com.google.android.material.appbar.AppBarLayout>
23-
24-
<com.devgary.contentview.ui.ContentView
25-
android:id="@+id/contentview"
23+
24+
<fragment
25+
android:id="@+id/fragment_container"
26+
android:name="com.devgary.contentviewdemo.util.DemoFragment"
2627
android:layout_width="match_parent"
27-
android:layout_height="wrap_content"
28+
android:layout_height="match_parent"
2829
app:layout_constraintBottom_toBottomOf="parent"
2930
app:layout_constraintStart_toStartOf="parent"
3031
app:layout_constraintEnd_toEndOf="parent"
3132
app:layout_constraintTop_toTopOf="parent" />
32-
3333
</androidx.constraintlayout.widget.ConstraintLayout>

0 commit comments

Comments
 (0)