Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

RecyclerView integration #13132

Merged
merged 3 commits into from
Nov 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ public class MapView extends FrameLayout implements NativeMapView.ViewCallback {
private MapKeyListener mapKeyListener;
@Nullable
private Bundle savedInstanceState;
private boolean isStarted;

@UiThread
public MapView(@NonNull Context context) {
Expand Down Expand Up @@ -345,8 +346,11 @@ public void onSaveInstanceState(@NonNull Bundle outState) {
*/
@UiThread
public void onStart() {
ConnectivityReceiver.instance(getContext()).activate();
FileSource.getInstance(getContext()).activate();
if (!isStarted) {
ConnectivityReceiver.instance(getContext()).activate();
FileSource.getInstance(getContext()).activate();
isStarted = true;
}
if (mapboxMap != null) {
mapboxMap.onStart();
}
Expand Down Expand Up @@ -391,8 +395,11 @@ public void onStop() {
mapRenderer.onStop();
}

ConnectivityReceiver.instance(getContext()).deactivate();
FileSource.getInstance(getContext()).deactivate();
if (isStarted) {
ConnectivityReceiver.instance(getContext()).deactivate();
FileSource.getInstance(getContext()).deactivate();
isStarted = false;
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -872,7 +872,28 @@
android:name="android.support.PARENT_ACTIVITY"
android:value=".activity.FeatureOverviewActivity" />
</activity>

<activity
android:name=".activity.maplayout.RecyclerViewActivity"
android:description="@string/description_recyclerview"
android:label="@string/activity_recyclerview">
<meta-data
android:name="@string/category"
android:value="@string/category_maplayout" />
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activity.FeatureOverviewActivity" />
</activity>
<activity
android:name=".activity.fragment.NestedViewPagerActivity"
android:description="@string/description_recyclerview"
android:label="@string/activity_nested_viewpager">
<meta-data
android:name="@string/category"
android:value="@string/category_fragment" />
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".activity.FeatureOverviewActivity" />
</activity>
<!-- For Instrumentation tests -->
<activity
android:name=".activity.style.RuntimeStyleTestActivity"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package com.mapbox.mapboxsdk.testapp.activity.fragment

import android.annotation.SuppressLint
import android.os.Bundle
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
import android.support.v4.app.FragmentStatePagerAdapter
import android.support.v4.view.ViewPager
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import com.mapbox.mapboxsdk.camera.CameraPosition
import com.mapbox.mapboxsdk.constants.Style
import com.mapbox.mapboxsdk.geometry.LatLng
import com.mapbox.mapboxsdk.maps.MapboxMapOptions
import com.mapbox.mapboxsdk.maps.SupportMapFragment
import com.mapbox.mapboxsdk.testapp.R
import kotlinx.android.synthetic.main.activity_recyclerview.*

/**
* TestActivity showcasing how to integrate a MapView in a RecyclerView.
* <p>
* It requires calling the correct lifecycle methods when detaching and attaching the View to
* the RecyclerView with onViewAttachedToWindow and onViewDetachedFromWindow.
* </p>
*/
@SuppressLint("ClickableViewAccessibility")
class NestedViewPagerActivity : AppCompatActivity() {
LukasPaczos marked this conversation as resolved.
Show resolved Hide resolved

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_recyclerview)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = ItemAdapter(LayoutInflater.from(this), supportFragmentManager)
}

class ItemAdapter(private val inflater: LayoutInflater, private val fragmentManager: FragmentManager) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

private val items = listOf(
"one", "two", "three", ViewPagerItem(), "four", "five", "six", "seven", "eight", "nine", "ten",
"eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen",
"nineteen", "twenty", "twenty-one"
)

private var mapHolder: ViewPagerHolder? = null

companion object {
const val TYPE_VIEWPAGER = 0
const val TYPE_TEXT = 1
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == TYPE_VIEWPAGER) {
val viewPager = inflater.inflate(R.layout.item_viewpager, parent, false) as ViewPager
mapHolder = ViewPagerHolder(viewPager, fragmentManager)
return mapHolder as ViewPagerHolder
} else {
TextHolder(inflater.inflate(android.R.layout.simple_list_item_1, parent, false) as TextView)
}
}

override fun getItemCount(): Int {
return items.count()
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder.itemViewType == TYPE_TEXT) {
val textHolder = holder as TextHolder
textHolder.bind(items[position] as String)
}
}

override fun getItemViewType(position: Int): Int {
return if (items[position] is ViewPagerItem) {
TYPE_VIEWPAGER
} else {
TYPE_TEXT
}
}

class TextHolder(val textView: TextView) : RecyclerView.ViewHolder(textView) {
fun bind(item: String) {
textView.text = item
}
}

class ViewPagerItem
class ViewPagerHolder(private val viewPager: ViewPager, fragmentManager: FragmentManager) : RecyclerView.ViewHolder(viewPager) {
init {
viewPager.adapter = MapPagerAdapter(fragmentManager)
viewPager.setOnTouchListener { view, motionEvent ->
// Disallow the touch request for recyclerView scroll
view.parent.requestDisallowInterceptTouchEvent(true)
viewPager.onTouchEvent(motionEvent)
false
}
}
}

class MapPagerAdapter(fm: FragmentManager?) : FragmentStatePagerAdapter(fm) {

override fun getItem(position: Int): Fragment {
val options = MapboxMapOptions()
options.textureMode(true)
options.doubleTapGesturesEnabled(false)
options.rotateGesturesEnabled(false)
options.tiltGesturesEnabled(false)
options.scrollGesturesEnabled(false)
options.zoomGesturesEnabled(false)
when (position) {
0 -> {
options.styleUrl(Style.MAPBOX_STREETS)
options.camera(CameraPosition.Builder().target(LatLng(34.920526, 102.634774)).zoom(3.0).build())
return SupportMapFragment.newInstance(options)
}
1 -> {
return EmptyFragment.newInstance()
}
2 -> {
options.styleUrl(Style.DARK)
options.camera(CameraPosition.Builder().target(LatLng(62.326440, 92.764913)).zoom(3.0).build())
return SupportMapFragment.newInstance(options)
}
3 -> {
return EmptyFragment.newInstance()
}
4 -> {
options.styleUrl(Style.SATELLITE)
options.camera(CameraPosition.Builder().target(LatLng(-25.007786, 133.623852)).zoom(3.0).build())
return SupportMapFragment.newInstance(options)
}
5 -> {
return EmptyFragment.newInstance()
}
}
throw IllegalAccessError()
}

override fun getCount(): Int {
return 6
}
}

class EmptyFragment : Fragment() {
companion object {
fun newInstance(): EmptyFragment {
return EmptyFragment()
}
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val textView = TextView(inflater.context)
textView.text = "This is an empty Fragment"
return textView
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
package com.mapbox.mapboxsdk.testapp.activity.maplayout

import android.annotation.SuppressLint
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.LinearLayoutManager
import android.support.v7.widget.RecyclerView
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.TextView
import com.mapbox.mapboxsdk.maps.MapView
import com.mapbox.mapboxsdk.testapp.R
import kotlinx.android.synthetic.main.activity_recyclerview.*

/**
* TestActivity showcasing how to integrate a MapView in a RecyclerView.
* <p>
* It requires calling the correct lifecycle methods when detaching and attaching the View to
* the RecyclerView with onViewAttachedToWindow and onViewDetachedFromWindow.
* </p>
*/
@SuppressLint("ClickableViewAccessibility")
class RecyclerViewActivity : AppCompatActivity() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This activity leaks context when rotated. Also, when map element is out of view and the activity is recreated (rotated), the map is not loading anymore.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looked into this with this PR and solution for this is #13133


override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_recyclerview)
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = ItemAdapter(LayoutInflater.from(this), savedInstanceState)
}

override fun onSaveInstanceState(outState: Bundle?) {
super.onSaveInstanceState(outState)
// to save state, we need to call MapView#onSaveInstanceState
(recyclerView.adapter as ItemAdapter).onSaveInstanceState(outState)
}

override fun onLowMemory() {
super.onLowMemory()
// to release memory, we need to call MapView#onLowMemory
(recyclerView.adapter as ItemAdapter).onLowMemory()
}

override fun onDestroy() {
super.onDestroy()
// to perform cleanup, we need to call MapView#onDestroy
(recyclerView.adapter as ItemAdapter).onDestroy()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like some of the problems mentioned above might stem from calling MapView#onDestroy only when activity is destroyed. We should call that whenever the MapView holder is destroyed? Maybe #onViewRecycled? Not sure here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue above was the view measurement from #13133 . View recycling still keeps the View in memory, it's detached but keeps it alive. OnDestroy doesn't match that step at that time completely. Destroying at view recycle time would also result in recreating it completely while scrolling up and down the list. This results in hick-up during scrolling and seeing the black surface for a second. I initially went with that approach but was not satisfied with the experience.

}

class ItemAdapter(private val inflater: LayoutInflater, val savedInstanceState: Bundle?) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

private val items = listOf(
"one", "two", "three", MapItem(), "four", "five", "six", "seven", "eight", "nine", "ten",
"eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen",
"nineteen", "twenty", "twenty-one"
)

private var mapHolder: MapHolder? = null

companion object {
const val TYPE_MAP = 0
const val TYPE_TEXT = 1
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return if (viewType == TYPE_MAP) {
val mapView = inflater.inflate(R.layout.item_map, parent, false) as MapView
mapHolder = MapHolder(mapView, savedInstanceState)
return mapHolder as MapHolder
} else {
TextHolder(inflater.inflate(android.R.layout.simple_list_item_1, parent, false) as TextView)
}
}

override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
super.onViewAttachedToWindow(holder)
if (holder is MapHolder) {
val mapView = holder.mapView
mapView.onStart()
mapView.onResume()
}
}

override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
super.onViewDetachedFromWindow(holder)
if (holder is MapHolder) {
val mapView = holder.mapView
mapView.onPause()
mapView.onStop()
}
}

override fun getItemCount(): Int {
return items.count()
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
if (holder.itemViewType == TYPE_TEXT) {
val textHolder = holder as TextHolder
textHolder.bind(items[position] as String)
}
}

override fun getItemViewType(position: Int): Int {
return if (items[position] is MapItem) {
TYPE_MAP
} else {
TYPE_TEXT
}
}

fun onSaveInstanceState(savedInstanceState: Bundle?){
savedInstanceState?.let {
mapHolder?.mapView?.onSaveInstanceState(it)
}
}

fun onLowMemory() {
mapHolder?.mapView?.onLowMemory()
}

fun onDestroy() {
mapHolder?.mapView?.let {
it.onPause()
it.onStop()
it.onDestroy()
}
}

class MapItem
class MapHolder(val mapView: MapView, bundle: Bundle?) : RecyclerView.ViewHolder(mapView) {
init {
mapView.onCreate(bundle)
mapView.setOnTouchListener { view, motionEvent ->
// Disallow the touch request for recyclerView scroll
view.parent.requestDisallowInterceptTouchEvent(true)
mapView.onTouchEvent(motionEvent)
true
}
}
}
class TextHolder(val textView: TextView) : RecyclerView.ViewHolder(textView) {
fun bind(item: String) {
textView.text = item
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />

</android.support.constraint.ConstraintLayout>
Loading