diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 168c6fd4..c2695837 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -23,7 +23,7 @@ tools:targetApi="31" tools:ignore="MissingTvBanner"> diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/DetailFragment.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/DetailFragment.kt deleted file mode 100644 index 981976ec..00000000 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/DetailFragment.kt +++ /dev/null @@ -1,158 +0,0 @@ -package com.rubensousa.dpadrecyclerview.sample - -import android.os.Bundle -import android.view.View -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.recyclerview.widget.ConcatAdapter -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.rubensousa.decorator.GridSpanMarginDecoration -import com.rubensousa.dpadrecyclerview.ChildAlignment -import com.rubensousa.dpadrecyclerview.DpadRecyclerView -import com.rubensousa.dpadrecyclerview.OnViewHolderSelectedListener -import com.rubensousa.dpadrecyclerview.ParentAlignment -import com.rubensousa.dpadrecyclerview.ParentAlignment.Edge -import com.rubensousa.dpadrecyclerview.sample.databinding.ScreenTvDetailBinding -import com.rubensousa.dpadrecyclerview.sample.item.ItemGridAdapter -import com.rubensousa.dpadrecyclerview.sample.item.ItemViewHolder -import com.rubensousa.dpadrecyclerview.sample.list.ListHeaderAdapter -import com.rubensousa.dpadrecyclerview.sample.list.ListPlaceholderAdapter -import timber.log.Timber - -class DetailFragment : Fragment(R.layout.screen_tv_detail) { - - private var _binding: ScreenTvDetailBinding? = null - private val binding: ScreenTvDetailBinding get() = _binding!! - private val topParentAlignment = ParentAlignment( - edge = Edge.NONE, - offset = 0, - offsetRatio = 0.05f - ) - private val topChildAlignment = ChildAlignment(offset = 0, offsetRatio = 0f) - private val centerParentAlignment = ParentAlignment( - edge = Edge.NONE, - offset = 0, - offsetRatio = 0.5f - ) - private val centerChildAlignment = ChildAlignment(offset = 0, offsetRatio = 0.5f) - private val viewModel by viewModels() - private val loadingAdapter = ListPlaceholderAdapter( - items = 5, - focusPlaceholders = true - ) - private val itemAdapter = ItemGridAdapter(object : ItemViewHolder.ItemClickListener { - override fun onViewHolderClicked() { - } - }) - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - _binding = ScreenTvDetailBinding.bind(view) - val recyclerView = binding.recyclerView - setupAdapter(recyclerView) - binding.up.setOnClickListener { - val subPosition = recyclerView.getSelectedSubPosition() - if (subPosition > 0) { - recyclerView.setSelectedSubPositionSmooth(subPosition - 1) - } else { - recyclerView.setSelectedPositionSmooth( - recyclerView.getSelectedPosition() - 1 - ) - } - } - binding.down.setOnClickListener { - val subPosition = recyclerView.getSelectedSubPosition() - val subPositionCount = recyclerView.getCurrentSubPositions() - if (subPosition < subPositionCount - 1) { - recyclerView.setSelectedSubPositionSmooth(subPosition + 1) - } else { - recyclerView.setSelectedPositionSmooth( - recyclerView.getSelectedPosition() + 1 - ) - } - } - viewModel.listState.observe(viewLifecycleOwner) { list -> - itemAdapter.submitList(list) - } - viewModel.loadingState.observe(viewLifecycleOwner) { isLoading -> - loadingAdapter.show(isLoading) - } - recyclerView.requestFocus() - } - - private fun setupAdapter(recyclerView: DpadRecyclerView) { - val concatAdapter = ConcatAdapter( - ConcatAdapter.Config.Builder() - .setIsolateViewTypes(true) - .build() - ) - val headerAdapter = ListHeaderAdapter() - headerAdapter.submitList(listOf("Header", "Header")) - concatAdapter.addAdapter(headerAdapter) - concatAdapter.addAdapter(itemAdapter) - concatAdapter.addAdapter(loadingAdapter) - recyclerView.setSpanSizeLookup(object : GridLayoutManager.SpanSizeLookup() { - override fun getSpanSize(position: Int): Int { - return if (position < headerAdapter.itemCount) { - recyclerView.getSpanCount() - } else { - 1 - } - } - }) - recyclerView.setChildAlignment(topChildAlignment) - recyclerView.setParentAlignment(topParentAlignment) - recyclerView.addItemDecoration( - GridSpanMarginDecoration.create( - margin = binding.root.context.resources.getDimensionPixelOffset( - R.dimen.item_spacing - ), - recyclerView.getDpadLayoutManager() - ) - ) - recyclerView.adapter = concatAdapter - recyclerView.addOnViewHolderSelectedListener(object : - OnViewHolderSelectedListener { - override fun onViewHolderSelected( - parent: RecyclerView, - child: RecyclerView.ViewHolder?, - position: Int, - subPosition: Int - ) { - if (position > 6) { - recyclerView.setAlignments( - centerParentAlignment, - centerChildAlignment, - smooth = true - ) - } else { - recyclerView.setAlignments( - topParentAlignment, - topChildAlignment, - smooth = true - ) - } - Timber.d("Selected: $position, $subPosition") - viewModel.loadMore(position) - } - - override fun onViewHolderSelectedAndAligned( - parent: RecyclerView, - child: RecyclerView.ViewHolder?, - position: Int, - subPosition: Int - ) { - Timber.d("Aligned: $position, $subPosition") - } - }) - } - - override fun onDestroyView() { - super.onDestroyView() - binding.recyclerView.adapter = null - _binding = null - } - - -} diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/MainActivity.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/MainActivity.kt deleted file mode 100644 index cce500cd..00000000 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/MainActivity.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.rubensousa.dpadrecyclerview.sample - -import androidx.appcompat.app.AppCompatActivity -import android.os.Bundle -import com.rubensousa.dpadrecyclerview.sample.R - -class MainActivity : AppCompatActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - } -} \ No newline at end of file diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/MainFragment.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/MainFragment.kt deleted file mode 100644 index 1cf71fff..00000000 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/MainFragment.kt +++ /dev/null @@ -1,120 +0,0 @@ -package com.rubensousa.dpadrecyclerview.sample - -import android.os.Bundle -import android.view.View -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.ConcatAdapter -import androidx.recyclerview.widget.RecyclerView -import com.rubensousa.decorator.DecorationLookup -import com.rubensousa.decorator.LinearMarginDecoration -import com.rubensousa.dpadrecyclerview.* -import com.rubensousa.dpadrecyclerview.sample.databinding.ScreenTvNestedListsBinding -import com.rubensousa.dpadrecyclerview.sample.item.ItemViewHolder -import com.rubensousa.dpadrecyclerview.sample.list.DpadStateHolder -import com.rubensousa.dpadrecyclerview.sample.list.ListPlaceholderAdapter -import com.rubensousa.dpadrecyclerview.sample.list.NestedListAdapter -import timber.log.Timber - -class MainFragment : Fragment(R.layout.screen_tv_nested_lists) { - - private var _binding: ScreenTvNestedListsBinding? = null - private val binding: ScreenTvNestedListsBinding get() = _binding!! - private var selectedPosition = RecyclerView.NO_POSITION - private val scrollStateHolder = DpadStateHolder() - private val viewModel by viewModels() - private val loadingAdapter = ListPlaceholderAdapter() - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - _binding = ScreenTvNestedListsBinding.bind(view) - val nestedListAdapter = setupAdapter() - setupAlignment(binding.recyclerView) - setupPagination(binding.recyclerView) - viewModel.listState.observe(viewLifecycleOwner) { list -> - nestedListAdapter.submitList(list) { - binding.recyclerView.invalidateItemDecorations() - } - } - viewModel.loadingState.observe(viewLifecycleOwner) { isLoading -> - loadingAdapter.show(isLoading) - } - binding.recyclerView.requestFocus() - if (selectedPosition != RecyclerView.NO_POSITION) { - binding.recyclerView.setSelectedPosition( - selectedPosition, - object : ViewHolderTask() { - override fun execute(viewHolder: RecyclerView.ViewHolder) { - Timber.d("Selection state restored") - } - }) - } - } - - private fun setupAdapter(): NestedListAdapter { - val concatAdapter = ConcatAdapter( - ConcatAdapter.Config.Builder() - .setIsolateViewTypes(true) - .build() - ) - val nestedListAdapter = NestedListAdapter(scrollStateHolder, - object : ItemViewHolder.ItemClickListener { - override fun onViewHolderClicked() { - findNavController().navigate(R.id.open_detail) - } - }) - concatAdapter.addAdapter(nestedListAdapter) - concatAdapter.addAdapter(loadingAdapter) - nestedListAdapter.stateRestorationPolicy = - RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY - binding.recyclerView.adapter = concatAdapter - return nestedListAdapter - } - - private fun setupPagination(recyclerView: DpadRecyclerView) { - recyclerView.addOnViewHolderSelectedListener(object : - OnViewHolderSelectedListener { - override fun onViewHolderSelected( - parent: RecyclerView, - child: RecyclerView.ViewHolder?, - position: Int, - subPosition: Int - ) { - selectedPosition = position - viewModel.loadMore(selectedPosition) - Timber.d("Selected: $position, $subPosition") - } - - override fun onViewHolderSelectedAndAligned( - parent: RecyclerView, - child: RecyclerView.ViewHolder?, - position: Int, - subPosition: Int - ) { - Timber.d("Aligned: $position, $subPosition") - } - }) - } - - private fun setupAlignment(recyclerView: DpadRecyclerView) { - recyclerView.addItemDecoration( - LinearMarginDecoration.createVertical( - verticalMargin = resources.getDimensionPixelOffset( - R.dimen.item_spacing - ), - decorationLookup = object : DecorationLookup { - override fun shouldApplyDecoration(position: Int, itemCount: Int): Boolean { - return position != itemCount - 1 || !loadingAdapter.isShowing() - } - } - ) - ) - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - -} \ No newline at end of file diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/item/ItemNestedAdapter.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/item/ItemNestedAdapter.kt deleted file mode 100644 index 652627da..00000000 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/item/ItemNestedAdapter.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.rubensousa.dpadrecyclerview.sample.item - -import android.annotation.SuppressLint -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.rubensousa.dpadrecyclerview.sample.databinding.AdapterNestedItemBinding - -class ItemNestedAdapter : RecyclerView.Adapter() { - - private var list: List = emptyList() - - var clickListener: ItemViewHolder.ItemClickListener? = null - - @SuppressLint("NotifyDataSetChanged") - fun replaceList(newList: List) { - list = newList - notifyDataSetChanged() - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { - val binding = AdapterNestedItemBinding.inflate( - LayoutInflater.from(parent.context), parent, false - ) - return ItemViewHolder(binding.root, binding.textView) - } - - override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { - holder.bind(list[position], clickListener) - } - - override fun onViewRecycled(holder: ItemViewHolder) { - super.onViewRecycled(holder) - holder.recycle() - } - - override fun getItemCount(): Int = list.size - - -} diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/list/ListModel.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/list/ListModel.kt deleted file mode 100644 index 12882cda..00000000 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/list/ListModel.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.rubensousa.dpadrecyclerview.sample.list - -data class ListModel(val title: String, val items: List) \ No newline at end of file diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/list/ListTypes.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/list/ListTypes.kt deleted file mode 100644 index de4bd6ea..00000000 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/list/ListTypes.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.rubensousa.dpadrecyclerview.sample.list - -object ListTypes { - const val LOADING = 0 - const val HEADER = 1 - const val ITEM = 2 -} \ No newline at end of file diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/list/ListViewHolder.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/list/ListViewHolder.kt deleted file mode 100644 index 7a16ee62..00000000 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/list/ListViewHolder.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.rubensousa.dpadrecyclerview.sample.list - -import androidx.recyclerview.widget.RecyclerView -import com.rubensousa.decorator.LinearMarginDecoration -import com.rubensousa.dpadrecyclerview.DpadRecyclerView -import com.rubensousa.dpadrecyclerview.DpadViewHolder -import com.rubensousa.dpadrecyclerview.sample.R -import com.rubensousa.dpadrecyclerview.sample.databinding.AdapterListBinding -import com.rubensousa.dpadrecyclerview.sample.item.ItemNestedAdapter -import com.rubensousa.dpadrecyclerview.sample.item.ItemViewHolder - -class ListViewHolder(private val binding: AdapterListBinding) : - RecyclerView.ViewHolder(binding.root), DpadViewHolder { - - private val adapter = ItemNestedAdapter() - private var key: String? = null - - init { - itemView.setOnFocusChangeListener { _, hasFocus -> - if (hasFocus) { - binding.recyclerView.requestFocus() - } - } - setupRecyclerView(binding.recyclerView) - onViewHolderDeselected() - } - - fun bind( - list: ListModel, stateHolder: DpadStateHolder, - clickListener: ItemViewHolder.ItemClickListener - ) { - adapter.clickListener = clickListener - key = list.title - binding.textView.text = list.title - adapter.replaceList(list.items) - binding.recyclerView.adapter = adapter - stateHolder.register(binding.recyclerView, list.title) - } - - fun onRecycled(stateHolder: DpadStateHolder) { - adapter.clickListener = null - key?.let { scrollKey -> - stateHolder.unregister(binding.recyclerView, scrollKey) - } - binding.recyclerView.adapter = null - } - - override fun onViewHolderSelected() { - super.onViewHolderSelected() - binding.recyclerView.alpha = 1.0f - binding.textView.alpha = 1.0f - } - - override fun onViewHolderDeselected() { - super.onViewHolderDeselected() - binding.recyclerView.alpha = 0.5f - binding.textView.alpha = 0.5f - } - - fun onAttachedToWindow() {} - - fun onDetachedFromWindow() {} - - private fun setupRecyclerView(recyclerView: DpadRecyclerView) { - recyclerView.apply { - addItemDecoration( - LinearMarginDecoration.createHorizontal( - horizontalMargin = binding.root.context.resources.getDimensionPixelOffset( - R.dimen.item_spacing - ) / 2 - ) - ) - } - } - -} - diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/list/NestedListAdapter.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/list/NestedListAdapter.kt deleted file mode 100644 index 05fd7363..00000000 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/list/NestedListAdapter.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.rubensousa.dpadrecyclerview.sample.list - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import com.rubensousa.dpadrecyclerview.sample.databinding.AdapterListBinding -import com.rubensousa.dpadrecyclerview.sample.item.ItemViewHolder - -class NestedListAdapter( - private val stateHolder: DpadStateHolder, - private val onItemClickListener: ItemViewHolder.ItemClickListener) : - ListAdapter(DIFF_CALLBACK) { - - companion object { - private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean { - return oldItem.title == newItem.title - } - - override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean { - return oldItem == newItem - } - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder { - return ListViewHolder( - AdapterListBinding.inflate(LayoutInflater.from(parent.context), parent, false) - ) - } - - override fun onBindViewHolder(holder: ListViewHolder, position: Int) { - holder.bind(getItem(position), stateHolder, onItemClickListener) - } - - override fun onViewRecycled(holder: ListViewHolder) { - super.onViewRecycled(holder) - holder.onRecycled(stateHolder) - } - - override fun onViewAttachedToWindow(holder: ListViewHolder) { - super.onViewAttachedToWindow(holder) - holder.onAttachedToWindow() - } - - override fun onViewDetachedFromWindow(holder: ListViewHolder) { - super.onViewDetachedFromWindow(holder) - holder.onDetachedFromWindow() - } - - -} \ No newline at end of file diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/MainActivity.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/MainActivity.kt new file mode 100644 index 00000000..0c7e7136 --- /dev/null +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/MainActivity.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2022 Rúben Sousa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rubensousa.dpadrecyclerview.sample.ui + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.rubensousa.dpadrecyclerview.sample.R + +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + } +} \ No newline at end of file diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/detail/DetailFragment.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/detail/DetailFragment.kt new file mode 100644 index 00000000..ec9b14d7 --- /dev/null +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/detail/DetailFragment.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2022 Rúben Sousa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rubensousa.dpadrecyclerview.sample.ui.screen.detail + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import com.rubensousa.dpadrecyclerview.sample.R +import com.rubensousa.dpadrecyclerview.sample.databinding.ScreenTvDetailBinding + +class DetailFragment : Fragment(R.layout.screen_tv_detail) { + + private var _binding: ScreenTvDetailBinding? = null + private val binding: ScreenTvDetailBinding get() = _binding!! + private val viewModel by viewModels() + private val listController = DetailListController(this) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + _binding = ScreenTvDetailBinding.bind(view) + val recyclerView = binding.recyclerView + listController.setup(recyclerView, viewLifecycleOwner, onSelected = { position -> + viewModel.loadMore(position) + }) + binding.up.setOnClickListener { + listController.scrollToPrevious() + } + binding.down.setOnClickListener { + listController.scrollToNext() + } + viewModel.listState.observe(viewLifecycleOwner) { list -> + listController.submitList(list) + } + viewModel.loadingState.observe(viewLifecycleOwner) { isLoading -> + listController.showLoading(isLoading) + } + + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + +} diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/detail/DetailListAlignment.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/detail/DetailListAlignment.kt new file mode 100644 index 00000000..6ae65150 --- /dev/null +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/detail/DetailListAlignment.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2022 Rúben Sousa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rubensousa.dpadrecyclerview.sample.ui.screen.detail + +import com.rubensousa.dpadrecyclerview.ChildAlignment +import com.rubensousa.dpadrecyclerview.DpadRecyclerView +import com.rubensousa.dpadrecyclerview.ParentAlignment +import timber.log.Timber + +class DetailListAlignment { + + private val topParentAlignment = ParentAlignment( + edge = ParentAlignment.Edge.NONE, + offset = 0, + offsetRatio = 0.05f + ) + private val topChildAlignment = ChildAlignment(offset = 0, offsetRatio = 0f) + private val centerParentAlignment = ParentAlignment( + edge = ParentAlignment.Edge.NONE, + offset = 0, + offsetRatio = 0.5f + ) + private val centerChildAlignment = ChildAlignment(offset = 0, offsetRatio = 0.5f) + private var isAlignedToTop = false + + fun alignToCenter(recyclerView: DpadRecyclerView) { + if (!isAlignedToTop){ + return + } + recyclerView.setAlignments(centerParentAlignment, centerChildAlignment, smooth = true) + Timber.i("Aligning to center") + isAlignedToTop = false + } + + fun alignToTop(recyclerView: DpadRecyclerView) { + if (isAlignedToTop) { + return + } + recyclerView.setAlignments(topParentAlignment, topChildAlignment, smooth = true) + Timber.i("Aligning to top") + isAlignedToTop = true + } + +} diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/detail/DetailListController.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/detail/DetailListController.kt new file mode 100644 index 00000000..f96c255b --- /dev/null +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/detail/DetailListController.kt @@ -0,0 +1,178 @@ +/* + * Copyright 2022 Rúben Sousa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rubensousa.dpadrecyclerview.sample.ui.screen.detail + +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.recyclerview.widget.ConcatAdapter +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.rubensousa.decorator.GridSpanMarginDecoration +import com.rubensousa.dpadrecyclerview.DpadRecyclerView +import com.rubensousa.dpadrecyclerview.OnViewHolderSelectedListener +import com.rubensousa.dpadrecyclerview.sample.R +import com.rubensousa.dpadrecyclerview.sample.ui.widgets.item.ItemGridAdapter +import com.rubensousa.dpadrecyclerview.sample.ui.widgets.item.ItemViewHolder +import com.rubensousa.dpadrecyclerview.sample.ui.widgets.list.ListHeaderAdapter +import com.rubensousa.dpadrecyclerview.sample.ui.widgets.list.ListPlaceholderAdapter + +class DetailListController(private val fragment: DetailFragment) { + + private val loadingAdapter = ListPlaceholderAdapter( + items = 5, + layoutId = R.layout.adapter_grid_placeholder, + focusPlaceholders = false + ) + private val itemAdapter = ItemGridAdapter(object : ItemViewHolder.ItemClickListener { + override fun onViewHolderClicked() { + } + }) + private var dpadRecyclerView: DpadRecyclerView? = null + private val alignment = DetailListAlignment() + private val headerAdapter = ListHeaderAdapter() + + init { + headerAdapter.submitList(listOf("Header")) + } + + fun setup( + recyclerView: DpadRecyclerView, + lifecycleOwner: LifecycleOwner, + onSelected: (position: Int) -> Unit + ) { + dpadRecyclerView = recyclerView + setupLayout(recyclerView) + setupLifecycle(lifecycleOwner) + setupAdapter(recyclerView) + setupAlignment(recyclerView) + setupPagination(recyclerView, onSelected) + alignment.alignToTop(recyclerView) + recyclerView.requestFocus() + } + + fun showLoading(isLoading: Boolean) { + loadingAdapter.show(isLoading) + } + + fun submitList(list: List) { + itemAdapter.submitList(list) + } + + fun scrollToNext() { + dpadRecyclerView?.let { recyclerView -> + val subPosition = recyclerView.getSelectedSubPosition() + val subPositionCount = recyclerView.getCurrentSubPositions() + if (subPosition < subPositionCount - 1) { + recyclerView.setSelectedSubPositionSmooth(subPosition + 1) + } else { + recyclerView.setSelectedPositionSmooth(recyclerView.getSelectedPosition() + 1) + } + } + } + + fun scrollToPrevious() { + dpadRecyclerView?.let { recyclerView -> + val subPosition = recyclerView.getSelectedSubPosition() + if (subPosition > 0) { + recyclerView.setSelectedSubPositionSmooth(subPosition - 1) + } else { + if (recyclerView.getSelectedPosition() == headerAdapter.itemCount) { + recyclerView.setSelectedSubPositionSmooth( + headerAdapter.itemCount - 1, headerAdapter.itemCount + ) + } else { + recyclerView.setSelectedPositionSmooth(recyclerView.getSelectedPosition() - 1) + } + } + } + } + + private fun setupLifecycle(lifecycleOwner: LifecycleOwner) { + lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + super.onDestroy(owner) + dpadRecyclerView?.adapter = null + dpadRecyclerView = null + } + }) + } + + private fun setupLayout(recyclerView: DpadRecyclerView) { + recyclerView.setSpanSizeLookup(object : GridLayoutManager.SpanSizeLookup() { + override fun getSpanSize(position: Int): Int { + return if (position < headerAdapter.itemCount) { + recyclerView.getSpanCount() + } else { + 1 + } + } + }) + recyclerView.addItemDecoration( + GridSpanMarginDecoration.create( + margin = recyclerView.resources.getDimensionPixelOffset(R.dimen.item_spacing), + recyclerView.getDpadLayoutManager() + ) + ) + } + + private fun setupPagination( + recyclerView: DpadRecyclerView, + onSelected: (position: Int) -> Unit + ) { + recyclerView.addOnViewHolderSelectedListener(object : OnViewHolderSelectedListener { + override fun onViewHolderSelected( + parent: RecyclerView, + child: RecyclerView.ViewHolder?, + position: Int, + subPosition: Int + ) { + onSelected(position) + } + }) + } + + private fun setupAdapter(recyclerView: DpadRecyclerView) { + val concatAdapter = ConcatAdapter( + ConcatAdapter.Config.Builder() + .setIsolateViewTypes(false) + .build() + ) + concatAdapter.addAdapter(headerAdapter) + concatAdapter.addAdapter(itemAdapter) + concatAdapter.addAdapter(loadingAdapter) + recyclerView.adapter = concatAdapter + } + + private fun setupAlignment(recyclerView: DpadRecyclerView) { + recyclerView.addOnViewHolderSelectedListener(object : + OnViewHolderSelectedListener { + override fun onViewHolderSelected( + parent: RecyclerView, + child: RecyclerView.ViewHolder?, + position: Int, + subPosition: Int + ) { + if (position >= headerAdapter.itemCount + recyclerView.getSpanCount()) { + alignment.alignToCenter(recyclerView) + } else { + alignment.alignToTop(recyclerView) + } + } + }) + } + +} diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/DetailViewModel.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/detail/DetailViewModel.kt similarity index 66% rename from sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/DetailViewModel.kt rename to sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/detail/DetailViewModel.kt index 69022ec5..3b7f2243 100644 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/DetailViewModel.kt +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/detail/DetailViewModel.kt @@ -1,4 +1,20 @@ -package com.rubensousa.dpadrecyclerview.sample +/* + * Copyright 2022 Rúben Sousa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rubensousa.dpadrecyclerview.sample.ui.screen.detail import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/main/MainFragment.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/main/MainFragment.kt new file mode 100644 index 00000000..13e728ba --- /dev/null +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/main/MainFragment.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2022 Rúben Sousa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rubensousa.dpadrecyclerview.sample.ui.screen.main + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import com.rubensousa.dpadrecyclerview.sample.R +import com.rubensousa.dpadrecyclerview.sample.databinding.ScreenTvNestedListsBinding + +class MainFragment : Fragment(R.layout.screen_tv_nested_lists) { + + private var _binding: ScreenTvNestedListsBinding? = null + private val binding: ScreenTvNestedListsBinding get() = _binding!! + private val viewModel by viewModels() + private val listController = MainListController(this) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + _binding = ScreenTvNestedListsBinding.bind(view) + listController.setup(binding.recyclerView, viewLifecycleOwner, onSelected = { position -> + viewModel.loadMore(position) + }) + viewModel.listState.observe(viewLifecycleOwner) { list -> + listController.submitList(list) + } + viewModel.loadingState.observe(viewLifecycleOwner) { isLoading -> + listController.showLoading(isLoading) + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + +} \ No newline at end of file diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/main/MainListController.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/main/MainListController.kt new file mode 100644 index 00000000..c70c62e2 --- /dev/null +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/main/MainListController.kt @@ -0,0 +1,150 @@ +/* + * Copyright 2022 Rúben Sousa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rubensousa.dpadrecyclerview.sample.ui.screen.main + +import androidx.fragment.app.Fragment +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.ConcatAdapter +import androidx.recyclerview.widget.RecyclerView +import com.rubensousa.decorator.DecorationLookup +import com.rubensousa.decorator.LinearMarginDecoration +import com.rubensousa.dpadrecyclerview.DpadRecyclerView +import com.rubensousa.dpadrecyclerview.OnViewHolderSelectedListener +import com.rubensousa.dpadrecyclerview.ViewHolderTask +import com.rubensousa.dpadrecyclerview.sample.R +import com.rubensousa.dpadrecyclerview.sample.ui.widgets.item.ItemViewHolder +import com.rubensousa.dpadrecyclerview.sample.ui.widgets.list.DpadStateHolder +import com.rubensousa.dpadrecyclerview.sample.ui.widgets.list.ListModel +import com.rubensousa.dpadrecyclerview.sample.ui.widgets.list.ListPlaceholderAdapter +import com.rubensousa.dpadrecyclerview.sample.ui.widgets.list.NestedListAdapter +import timber.log.Timber + +class MainListController(private val fragment: Fragment) { + + private var selectedPosition = RecyclerView.NO_POSITION + private val loadingAdapter = ListPlaceholderAdapter() + private val scrollStateHolder = DpadStateHolder() + private val nestedListAdapter = NestedListAdapter(scrollStateHolder, + object : ItemViewHolder.ItemClickListener { + override fun onViewHolderClicked() { + fragment.findNavController().navigate(R.id.open_detail) + } + }) + private var dpadRecyclerView: DpadRecyclerView? = null + + fun setup( + recyclerView: DpadRecyclerView, + lifecycleOwner: LifecycleOwner, + onSelected: (position: Int) -> Unit + ) { + dpadRecyclerView = recyclerView + setupAdapter(recyclerView) + setupSpacings(recyclerView) + setupPagination(recyclerView, onSelected) + setupLifecycle(lifecycleOwner) + + if (selectedPosition != RecyclerView.NO_POSITION) { + recyclerView.setSelectedPosition( + selectedPosition, object : ViewHolderTask() { + override fun execute(viewHolder: RecyclerView.ViewHolder) { + Timber.d("Selection state restored") + } + }) + } + + recyclerView.requestFocus() + } + + fun submitList(list: List) { + nestedListAdapter.submitList(list) { + dpadRecyclerView?.invalidateItemDecorations() + } + } + + fun showLoading(isLoading: Boolean) { + loadingAdapter.show(isLoading) + } + + private fun setupAdapter(recyclerView: DpadRecyclerView) { + val concatAdapter = ConcatAdapter( + ConcatAdapter.Config.Builder() + .setIsolateViewTypes(true) + .build() + ) + concatAdapter.addAdapter(nestedListAdapter) + concatAdapter.addAdapter(loadingAdapter) + nestedListAdapter.stateRestorationPolicy = + RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY + recyclerView.adapter = concatAdapter + } + + private fun setupLifecycle(lifecycleOwner: LifecycleOwner) { + lifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + super.onDestroy(owner) + dpadRecyclerView?.adapter = null + dpadRecyclerView = null + } + }) + } + + private fun setupPagination( + recyclerView: DpadRecyclerView, + onSelected: (position: Int) -> Unit + ) { + recyclerView.addOnViewHolderSelectedListener(object : + OnViewHolderSelectedListener { + override fun onViewHolderSelected( + parent: RecyclerView, + child: RecyclerView.ViewHolder?, + position: Int, + subPosition: Int + ) { + selectedPosition = position + onSelected(position) + Timber.d("Selected: $position, $subPosition") + } + + override fun onViewHolderSelectedAndAligned( + parent: RecyclerView, + child: RecyclerView.ViewHolder?, + position: Int, + subPosition: Int + ) { + Timber.d("Aligned: $position, $subPosition") + } + }) + } + + private fun setupSpacings(recyclerView: DpadRecyclerView) { + recyclerView.addItemDecoration( + LinearMarginDecoration.createVertical( + verticalMargin = recyclerView.resources.getDimensionPixelOffset( + R.dimen.item_spacing + ), + decorationLookup = object : DecorationLookup { + override fun shouldApplyDecoration(position: Int, itemCount: Int): Boolean { + return position != itemCount - 1 || !loadingAdapter.isShowing() + } + } + ) + ) + } + +} diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/MainViewModel.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/main/MainViewModel.kt similarity index 51% rename from sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/MainViewModel.kt rename to sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/main/MainViewModel.kt index a104900f..cbdc331e 100644 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/MainViewModel.kt +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/screen/main/MainViewModel.kt @@ -1,10 +1,26 @@ -package com.rubensousa.dpadrecyclerview.sample +/* + * Copyright 2022 Rúben Sousa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rubensousa.dpadrecyclerview.sample.ui.screen.main import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.rubensousa.dpadrecyclerview.sample.list.ListModel +import com.rubensousa.dpadrecyclerview.sample.ui.widgets.list.ListModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -18,9 +34,7 @@ class MainViewModel : ViewModel() { val listState: LiveData> = listLiveData init { - for (i in 0 until 3) { - list.add(generateList("List $i")) - } + appendNewPage() listLiveData.postValue(ArrayList(list)) } @@ -35,21 +49,27 @@ class MainViewModel : ViewModel() { loadingStateLiveData.postValue(true) viewModelScope.launch(Dispatchers.Default) { - for (i in 0 until 2) { - list.add(generateList("List ${list.size}")) - } + appendNewPage() delay(1000L) listLiveData.postValue(ArrayList(list)) }.invokeOnCompletion { loadingStateLiveData.postValue(false) } } - private fun generateList(title: String): ListModel { + private fun appendNewPage() { + repeat(3) { + list.add(generateList("List ${list.size}", centerAligned = true)) + list.add(generateList("List ${list.size}")) + list.add(generateList("List ${list.size}")) + } + } + + private fun generateList(title: String, centerAligned: Boolean = false): ListModel { val items = ArrayList() repeat(100) { items.add(it) } - return ListModel(title, items) + return ListModel(title, items, centerAligned) } diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/item/ItemGridAdapter.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/item/ItemGridAdapter.kt similarity index 65% rename from sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/item/ItemGridAdapter.kt rename to sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/item/ItemGridAdapter.kt index b11870db..f0062294 100644 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/item/ItemGridAdapter.kt +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/item/ItemGridAdapter.kt @@ -1,11 +1,27 @@ -package com.rubensousa.dpadrecyclerview.sample.item +/* + * Copyright 2022 Rúben Sousa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rubensousa.dpadrecyclerview.sample.ui.widgets.item import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import com.rubensousa.dpadrecyclerview.sample.databinding.AdapterItemBinding -import com.rubensousa.dpadrecyclerview.sample.list.ListTypes +import com.rubensousa.dpadrecyclerview.sample.ui.widgets.list.ListTypes class ItemGridAdapter(private val onItemClickListener: ItemViewHolder.ItemClickListener) : ListAdapter(DIFF_CALLBACK) { diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/item/ItemNestedAdapter.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/item/ItemNestedAdapter.kt new file mode 100644 index 00000000..7155b31c --- /dev/null +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/item/ItemNestedAdapter.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2022 Rúben Sousa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rubensousa.dpadrecyclerview.sample.ui.widgets.item + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.rubensousa.dpadrecyclerview.sample.R + +class ItemNestedAdapter( + private val layoutId: Int, + private val animateFocusChanges: Boolean +) : RecyclerView.Adapter() { + + private var list: List = emptyList() + + var clickListener: ItemViewHolder.ItemClickListener? = null + + @SuppressLint("NotifyDataSetChanged") + fun replaceList(newList: List) { + list = newList + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { + val view = LayoutInflater.from(parent.context).inflate(layoutId, parent, false) + return ItemViewHolder(view, view.findViewById(R.id.textView), animateFocusChanges) + } + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + holder.bind(list[position], clickListener) + } + + override fun onViewRecycled(holder: ItemViewHolder) { + super.onViewRecycled(holder) + holder.recycle() + } + + override fun getItemCount(): Int = list.size + + +} diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/item/ItemViewHolder.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/item/ItemViewHolder.kt similarity index 60% rename from sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/item/ItemViewHolder.kt rename to sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/item/ItemViewHolder.kt index ed19d476..34a95f60 100644 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/item/ItemViewHolder.kt +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/item/ItemViewHolder.kt @@ -1,4 +1,20 @@ -package com.rubensousa.dpadrecyclerview.sample.item +/* + * Copyright 2022 Rúben Sousa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rubensousa.dpadrecyclerview.sample.ui.widgets.item import android.view.View import android.view.animation.AccelerateDecelerateInterpolator @@ -8,7 +24,8 @@ import com.rubensousa.dpadrecyclerview.DpadViewHolder class ItemViewHolder( root: View, - private val textView: TextView + private val textView: TextView, + private val animateFocusChanges: Boolean = true ) : RecyclerView.ViewHolder(root), DpadViewHolder { private var clickListener: ItemClickListener? = null @@ -19,6 +36,9 @@ class ItemViewHolder( clickListener?.onViewHolderClicked() } root.setOnFocusChangeListener { _, hasFocus -> + if (!animateFocusChanges) { + return@setOnFocusChangeListener + } if (hasFocus) { grow() } else { diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/list/DpadStateHolder.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/list/DpadStateHolder.kt similarity index 66% rename from sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/list/DpadStateHolder.kt rename to sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/list/DpadStateHolder.kt index 7d2f139e..d01833fa 100644 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/list/DpadStateHolder.kt +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/list/DpadStateHolder.kt @@ -1,4 +1,20 @@ -package com.rubensousa.dpadrecyclerview.sample.list +/* + * Copyright 2022 Rúben Sousa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rubensousa.dpadrecyclerview.sample.ui.widgets.list import androidx.recyclerview.widget.RecyclerView import com.rubensousa.dpadrecyclerview.DpadRecyclerView diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/list/ListHeaderAdapter.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/list/ListHeaderAdapter.kt similarity index 79% rename from sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/list/ListHeaderAdapter.kt rename to sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/list/ListHeaderAdapter.kt index d4c5d906..376154f7 100644 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/list/ListHeaderAdapter.kt +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/list/ListHeaderAdapter.kt @@ -1,4 +1,20 @@ -package com.rubensousa.dpadrecyclerview.sample.list +/* + * Copyright 2022 Rúben Sousa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rubensousa.dpadrecyclerview.sample.ui.widgets.list import android.view.LayoutInflater import android.view.ViewGroup @@ -67,14 +83,6 @@ class ListHeaderAdapter : ListAdapter(DIFF_CALLBAC focusViewId = R.id.subPosition1TextView ) ) - add( - ViewHolderAlignment( - offset = 0, - offsetRatio = 0f, - alignmentViewId = R.id.subPosition0TextView, - focusViewId = R.id.subPosition2TextView - ) - ) } } diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/list/ListModel.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/list/ListModel.kt new file mode 100644 index 00000000..30fc3322 --- /dev/null +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/list/ListModel.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2022 Rúben Sousa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rubensousa.dpadrecyclerview.sample.ui.widgets.list + +data class ListModel(val title: String, val items: List, val centerAligned: Boolean) \ No newline at end of file diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/list/ListPlaceholderAdapter.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/list/ListPlaceholderAdapter.kt similarity index 63% rename from sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/list/ListPlaceholderAdapter.kt rename to sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/list/ListPlaceholderAdapter.kt index 12a14aaf..51870f79 100644 --- a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/list/ListPlaceholderAdapter.kt +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/list/ListPlaceholderAdapter.kt @@ -1,4 +1,20 @@ -package com.rubensousa.dpadrecyclerview.sample.list +/* + * Copyright 2022 Rúben Sousa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rubensousa.dpadrecyclerview.sample.ui.widgets.list import android.view.LayoutInflater import android.view.View @@ -18,8 +34,8 @@ class ListPlaceholderAdapter( val view = LayoutInflater.from(parent.context).inflate( layoutId, parent, false ) - view.isFocusableInTouchMode = focusPlaceholders view.isFocusable = focusPlaceholders + view.isFocusableInTouchMode = focusPlaceholders return VH(view) } @@ -28,11 +44,13 @@ class ListPlaceholderAdapter( } fun show(enabled: Boolean) { - val wasShowing = show + if (enabled == show) { + return + } show = enabled - if (!wasShowing && show) { + if (show) { notifyItemRangeInserted(0, items) - } else if (wasShowing && !show) { + } else { notifyItemRangeRemoved(0, items) } } diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/list/ListTypes.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/list/ListTypes.kt new file mode 100644 index 00000000..724e216d --- /dev/null +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/list/ListTypes.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2022 Rúben Sousa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rubensousa.dpadrecyclerview.sample.ui.widgets.list + +object ListTypes { + const val LOADING = 0 + const val HEADER = 1 + const val ITEM = 2 + const val LIST_CENTER = 3 + const val LIST_START = 4 +} \ No newline at end of file diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/list/ListViewHolder.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/list/ListViewHolder.kt new file mode 100644 index 00000000..50b14217 --- /dev/null +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/list/ListViewHolder.kt @@ -0,0 +1,99 @@ +/* + * Copyright 2022 Rúben Sousa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rubensousa.dpadrecyclerview.sample.ui.widgets.list + +import android.view.View +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.rubensousa.decorator.LinearMarginDecoration +import com.rubensousa.dpadrecyclerview.DpadRecyclerView +import com.rubensousa.dpadrecyclerview.DpadViewHolder +import com.rubensousa.dpadrecyclerview.sample.R +import com.rubensousa.dpadrecyclerview.sample.ui.widgets.item.ItemNestedAdapter +import com.rubensousa.dpadrecyclerview.sample.ui.widgets.item.ItemViewHolder + +class ListViewHolder(view: View, itemLayoutId: Int = R.layout.adapter_nested_item_start) : + RecyclerView.ViewHolder(view), DpadViewHolder { + + private val recyclerView = view.findViewById(R.id.recyclerView) + private val textView = view.findViewById(R.id.textView) + private val adapter = ItemNestedAdapter( + itemLayoutId, + animateFocusChanges = itemLayoutId == R.layout.adapter_nested_item_start + ) + private var key: String? = null + + init { + itemView.setOnFocusChangeListener { _, hasFocus -> + if (hasFocus) { + recyclerView.requestFocus() + } + } + setupRecyclerView(recyclerView) + onViewHolderDeselected() + } + + fun bind( + list: ListModel, stateHolder: DpadStateHolder, + clickListener: ItemViewHolder.ItemClickListener + ) { + adapter.clickListener = clickListener + key = list.title + textView.text = list.title + adapter.replaceList(list.items) + recyclerView.adapter = adapter + stateHolder.register(recyclerView, list.title) + } + + fun onRecycled(stateHolder: DpadStateHolder) { + adapter.clickListener = null + key?.let { scrollKey -> + stateHolder.unregister(recyclerView, scrollKey) + } + recyclerView.adapter = null + } + + override fun onViewHolderSelected() { + super.onViewHolderSelected() + recyclerView.alpha = 1.0f + textView.alpha = 1.0f + } + + override fun onViewHolderDeselected() { + super.onViewHolderDeselected() + recyclerView.alpha = 0.5f + textView.alpha = 0.5f + } + + fun onAttachedToWindow() {} + + fun onDetachedFromWindow() {} + + private fun setupRecyclerView(recyclerView: DpadRecyclerView) { + recyclerView.apply { + addItemDecoration( + LinearMarginDecoration.createHorizontal( + horizontalMargin = itemView.resources.getDimensionPixelOffset( + R.dimen.item_spacing + ) / 2 + ) + ) + } + } + +} + diff --git a/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/list/NestedListAdapter.kt b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/list/NestedListAdapter.kt new file mode 100644 index 00000000..a04c4642 --- /dev/null +++ b/sample/src/main/java/com/rubensousa/dpadrecyclerview/sample/ui/widgets/list/NestedListAdapter.kt @@ -0,0 +1,87 @@ +/* + * Copyright 2022 Rúben Sousa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.rubensousa.dpadrecyclerview.sample.ui.widgets.list + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import com.rubensousa.dpadrecyclerview.sample.R +import com.rubensousa.dpadrecyclerview.sample.ui.widgets.item.ItemViewHolder + +class NestedListAdapter( + private val stateHolder: DpadStateHolder, + private val onItemClickListener: ItemViewHolder.ItemClickListener +) : ListAdapter(DIFF_CALLBACK) { + + companion object { + private val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel): Boolean { + return oldItem.title == newItem.title + } + + override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean { + return oldItem == newItem + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder { + val layoutId: Int + val itemLayoutId: Int + if (viewType == ListTypes.LIST_CENTER) { + layoutId = R.layout.adapter_list_center + itemLayoutId = R.layout.adapter_nested_item_center + } else { + layoutId = R.layout.adapter_list_start + itemLayoutId = R.layout.adapter_nested_item_start + } + return ListViewHolder( + LayoutInflater.from(parent.context).inflate(layoutId, parent, false), + itemLayoutId + ) + } + + override fun getItemViewType(position: Int): Int { + val item = getItem(position) + if (item.centerAligned) { + return ListTypes.LIST_CENTER + } + return ListTypes.LIST_START + } + + override fun onBindViewHolder(holder: ListViewHolder, position: Int) { + holder.bind(getItem(position), stateHolder, onItemClickListener) + } + + override fun onViewRecycled(holder: ListViewHolder) { + super.onViewRecycled(holder) + holder.onRecycled(stateHolder) + } + + override fun onViewAttachedToWindow(holder: ListViewHolder) { + super.onViewAttachedToWindow(holder) + holder.onAttachedToWindow() + } + + override fun onViewDetachedFromWindow(holder: ListViewHolder) { + super.onViewDetachedFromWindow(holder) + holder.onDetachedFromWindow() + } + + +} \ No newline at end of file diff --git a/sample/src/main/res/layout/adapter_grid_placeholder.xml b/sample/src/main/res/layout/adapter_grid_placeholder.xml new file mode 100644 index 00000000..a15402ec --- /dev/null +++ b/sample/src/main/res/layout/adapter_grid_placeholder.xml @@ -0,0 +1,6 @@ + + + diff --git a/sample/src/main/res/layout/adapter_list_center.xml b/sample/src/main/res/layout/adapter_list_center.xml new file mode 100644 index 00000000..171a24c6 --- /dev/null +++ b/sample/src/main/res/layout/adapter_list_center.xml @@ -0,0 +1,32 @@ + + + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/layout/adapter_list_header.xml b/sample/src/main/res/layout/adapter_list_header.xml index 2d66b421..44cd3d45 100644 --- a/sample/src/main/res/layout/adapter_list_header.xml +++ b/sample/src/main/res/layout/adapter_list_header.xml @@ -33,20 +33,4 @@ android:textColor="@color/list_item_text" android:textSize="35sp" /> - - - - diff --git a/sample/src/main/res/layout/adapter_list.xml b/sample/src/main/res/layout/adapter_list_start.xml similarity index 100% rename from sample/src/main/res/layout/adapter_list.xml rename to sample/src/main/res/layout/adapter_list_start.xml diff --git a/sample/src/main/res/layout/adapter_nested_item_center.xml b/sample/src/main/res/layout/adapter_nested_item_center.xml new file mode 100644 index 00000000..053064e6 --- /dev/null +++ b/sample/src/main/res/layout/adapter_nested_item_center.xml @@ -0,0 +1,22 @@ + + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/layout/adapter_nested_item.xml b/sample/src/main/res/layout/adapter_nested_item_start.xml similarity index 100% rename from sample/src/main/res/layout/adapter_nested_item.xml rename to sample/src/main/res/layout/adapter_nested_item_start.xml diff --git a/sample/src/main/res/navigation/nav_graph.xml b/sample/src/main/res/navigation/nav_graph.xml index 14d4a35d..0c14aca8 100644 --- a/sample/src/main/res/navigation/nav_graph.xml +++ b/sample/src/main/res/navigation/nav_graph.xml @@ -6,7 +6,7 @@ + android:name="com.rubensousa.dpadrecyclerview.sample.ui.screen.main.MainFragment"> + android:name="com.rubensousa.dpadrecyclerview.sample.ui.screen.detail.DetailFragment" /> diff --git a/sample/src/main/res/values/colors.xml b/sample/src/main/res/values/colors.xml index f8c6127d..428f8265 100644 --- a/sample/src/main/res/values/colors.xml +++ b/sample/src/main/res/values/colors.xml @@ -6,5 +6,6 @@ #FF03DAC5 #FF018786 #FF000000 + #4B4B4B #FFFFFFFF \ No newline at end of file