Convert UserSearchFragment to kotlin and fix UserSearchMode

This commit is contained in:
Ammar Githam 2021-07-07 16:29:44 +09:00
parent 751259d430
commit 64600ceb04
4 changed files with 254 additions and 314 deletions

View File

@ -1,312 +0,0 @@
package awais.instagrabber.fragments;
import android.content.Context;
import android.os.Bundle;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.core.util.Pair;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.SavedStateHandle;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavBackStackEntry;
import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.transition.TransitionManager;
import com.google.android.material.chip.Chip;
import com.google.android.material.snackbar.Snackbar;
import java.util.Objects;
import java.util.Set;
import awais.instagrabber.activities.MainActivity;
import awais.instagrabber.adapters.UserSearchResultsAdapter;
import awais.instagrabber.customviews.helpers.TextWatcherAdapter;
import awais.instagrabber.databinding.FragmentUserSearchBinding;
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient;
import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
import awais.instagrabber.utils.ViewUtils;
import awais.instagrabber.viewmodels.UserSearchViewModel;
public class UserSearchFragment extends Fragment {
private static final String TAG = UserSearchFragment.class.getSimpleName();
private FragmentUserSearchBinding binding;
private UserSearchViewModel viewModel;
private UserSearchResultsAdapter resultsAdapter;
private int paddingOffset;
private final int windowWidth = Utils.displayMetrics.widthPixels;
private final int minInputWidth = Utils.convertDpToPx(50);
private String actionLabel;
private String title;
private boolean multiple;
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) {
binding = FragmentUserSearchBinding.inflate(inflater, container, false);
viewModel = new ViewModelProvider(this).get(UserSearchViewModel.class);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
paddingOffset = binding.search.getPaddingStart() + binding.search.getPaddingEnd() + binding.group
.getPaddingStart() + binding.group.getPaddingEnd() + binding.group.getChipSpacingHorizontal();
init();
}
@Override
public void onDestroyView() {
super.onDestroyView();
viewModel.cleanup();
}
private void init() {
final Bundle arguments = getArguments();
if (arguments != null) {
final UserSearchFragmentArgs fragmentArgs = UserSearchFragmentArgs.fromBundle(arguments);
actionLabel = fragmentArgs.getActionLabel();
title = fragmentArgs.getTitle();
multiple = fragmentArgs.getMultiple();
viewModel.setHideThreadIds(fragmentArgs.getHideThreadIds());
viewModel.setHideUserIds(fragmentArgs.getHideUserIds());
viewModel.setSearchMode(fragmentArgs.getSearchMode());
viewModel.setShowGroups(fragmentArgs.getShowGroups());
}
setupTitles();
setupInput();
setupResults();
setupObservers();
// show cached results
viewModel.showCachedResults();
}
private void setupTitles() {
if (!TextUtils.isEmpty(actionLabel)) {
binding.done.setText(actionLabel);
}
if (!TextUtils.isEmpty(title)) {
final MainActivity activity = (MainActivity) getActivity();
if (activity != null) {
final ActionBar actionBar = activity.getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle(title);
}
}
}
}
private void setupResults() {
final Context context = getContext();
if (context == null) return;
binding.results.setLayoutManager(new LinearLayoutManager(context));
resultsAdapter = new UserSearchResultsAdapter(multiple, (position, recipient, selected) -> {
if (!multiple) {
final NavController navController = NavHostFragment.findNavController(this);
if (!setResult(navController, recipient)) return;
navController.navigateUp();
return;
}
viewModel.setSelectedRecipient(recipient, !selected);
resultsAdapter.setSelectedRecipient(recipient, !selected);
if (!selected) {
createChip(recipient);
return;
}
final View chip = findChip(recipient);
if (chip == null) return;
removeChipFromGroup(chip);
});
binding.results.setAdapter(resultsAdapter);
binding.done.setOnClickListener(v -> {
final NavController navController = NavHostFragment.findNavController(this);
if (!setResult(navController, viewModel.getSelectedRecipients())) return;
navController.navigateUp();
});
}
private boolean setResult(@NonNull final NavController navController, final RankedRecipient rankedRecipient) {
final NavBackStackEntry navBackStackEntry = navController.getPreviousBackStackEntry();
if (navBackStackEntry == null) return false;
final SavedStateHandle savedStateHandle = navBackStackEntry.getSavedStateHandle();
savedStateHandle.set("result", rankedRecipient);
return true;
}
private boolean setResult(@NonNull final NavController navController, final Set<RankedRecipient> rankedRecipients) {
final NavBackStackEntry navBackStackEntry = navController.getPreviousBackStackEntry();
if (navBackStackEntry == null) return false;
final SavedStateHandle savedStateHandle = navBackStackEntry.getSavedStateHandle();
savedStateHandle.set("result", rankedRecipients);
return true;
}
private void setupInput() {
binding.search.addTextChangedListener(new TextWatcherAdapter() {
@Override
public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
// if (TextUtils.isEmpty(s)) {
// viewModel.cancelSearch();
// viewModel.clearResults();
// return;
// }
viewModel.search(s == null ? null : s.toString().trim());
}
});
binding.search.setOnKeyListener((v, keyCode, event) -> {
if (event != null && event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
final View chip = getLastChip();
if (chip == null) return false;
removeChip(chip);
}
return false;
});
binding.group.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() {
@Override
public void onChildViewAdded(final View parent, final View child) {}
@Override
public void onChildViewRemoved(final View parent, final View child) {
binding.group.post(() -> {
TransitionManager.beginDelayedTransition(binding.getRoot());
calculateInputWidth(0);
});
}
});
}
private void setupObservers() {
viewModel.getRecipients().observe(getViewLifecycleOwner(), results -> {
if (results == null) return;
switch (results.status) {
case SUCCESS:
if (results.data != null) {
resultsAdapter.submitList(results.data);
}
break;
case ERROR:
if (results.message != null) {
Snackbar.make(binding.getRoot(), results.message, Snackbar.LENGTH_LONG).show();
}
if (results.resId != 0) {
Snackbar.make(binding.getRoot(), results.resId, Snackbar.LENGTH_LONG).show();
}
if (results.data != null) {
resultsAdapter.submitList(results.data);
}
break;
case LOADING:
//noinspection DuplicateBranchesInSwitch
if (results.data != null) {
resultsAdapter.submitList(results.data);
}
break;
}
});
viewModel.showAction().observe(getViewLifecycleOwner(), showAction -> binding.done.setVisibility(showAction ? View.VISIBLE : View.GONE));
}
private void createChip(final RankedRecipient recipient) {
final Context context = getContext();
if (context == null) return;
final Chip chip = new Chip(context);
chip.setTag(recipient);
chip.setText(getRecipientText(recipient));
chip.setCloseIconVisible(true);
chip.setOnCloseIconClickListener(v -> removeChip(chip));
binding.group.post(() -> {
final Pair<Integer, Integer> measure = ViewUtils.measure(chip, binding.group);
TransitionManager.beginDelayedTransition(binding.getRoot());
calculateInputWidth(measure.second != null ? measure.second : 0);
binding.group.addView(chip, binding.group.getChildCount() - 1);
});
}
private String getRecipientText(final RankedRecipient recipient) {
if (recipient == null) return null;
if (recipient.getUser() != null) {
return recipient.getUser().getFullName();
}
if (recipient.getThread() != null) {
return recipient.getThread().getThreadTitle();
}
return null;
}
private void removeChip(@NonNull final View chip) {
final RankedRecipient recipient = (RankedRecipient) chip.getTag();
if (recipient == null) return;
viewModel.setSelectedRecipient(recipient, false);
resultsAdapter.setSelectedRecipient(recipient, false);
removeChipFromGroup(chip);
}
private View findChip(final RankedRecipient recipient) {
if (recipient == null || recipient.getUser() == null && recipient.getThread() == null) return null;
boolean isUser = recipient.getUser() != null;
final int childCount = binding.group.getChildCount();
if (childCount == 0) return null;
for (int i = childCount - 1; i >= 0; i--) {
final View child = binding.group.getChildAt(i);
if (child == null) continue;
final RankedRecipient tag = (RankedRecipient) child.getTag();
if (tag == null || isUser && tag.getUser() == null || !isUser && tag.getThread() == null) continue;
if ((isUser && tag.getUser().getPk() == recipient.getUser().getPk())
|| (!isUser && Objects.equals(tag.getThread().getThreadId(), recipient.getThread().getThreadId()))) {
return child;
}
}
return null;
}
private void removeChipFromGroup(final View chip) {
binding.group.post(() -> {
TransitionManager.beginDelayedTransition(binding.getRoot());
binding.group.removeView(chip);
});
}
private void calculateInputWidth(final int newChipWidth) {
final View lastChip = getLastChip();
int lastRight = lastChip != null ? lastChip.getRight() : 0;
final int remainingSpaceInRow = windowWidth - lastRight;
if (remainingSpaceInRow < newChipWidth) {
// next chip will go to the next row, so assume no chips present
lastRight = 0;
}
final int newRight = lastRight + newChipWidth;
final int newInputWidth = windowWidth - newRight - paddingOffset;
binding.search.getLayoutParams().width = newInputWidth < minInputWidth ? windowWidth : newInputWidth;
binding.search.requestLayout();
}
private View getLastChip() {
final int childCount = binding.group.getChildCount();
if (childCount == 0) {
return null;
}
for (int i = childCount - 1; i >= 0; i--) {
final View child = binding.group.getChildAt(i);
if (child instanceof Chip) {
return child;
}
}
return null;
}
}

View File

@ -0,0 +1,252 @@
package awais.instagrabber.fragments
import android.os.Bundle
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.OnHierarchyChangeListener
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionManager
import awais.instagrabber.activities.MainActivity
import awais.instagrabber.adapters.UserSearchResultsAdapter
import awais.instagrabber.customviews.helpers.TextWatcherAdapter
import awais.instagrabber.databinding.FragmentUserSearchBinding
import awais.instagrabber.models.Resource
import awais.instagrabber.repositories.responses.directmessages.RankedRecipient
import awais.instagrabber.utils.Utils
import awais.instagrabber.utils.extensions.trimAll
import awais.instagrabber.utils.measure
import awais.instagrabber.viewmodels.UserSearchViewModel
import com.google.android.material.chip.Chip
import com.google.android.material.snackbar.Snackbar
class UserSearchFragment : Fragment() {
private lateinit var binding: FragmentUserSearchBinding
private var resultsAdapter: UserSearchResultsAdapter? = null
private var paddingOffset = 0
private var actionLabel: String? = null
private var title: String? = null
private var multiple = false
private val viewModel: UserSearchViewModel by viewModels()
private val windowWidth = Utils.displayMetrics.widthPixels
private val minInputWidth = Utils.convertDpToPx(50f)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
binding = FragmentUserSearchBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
paddingOffset = with(binding) {
search.paddingStart + search.paddingEnd + group.paddingStart + group.paddingEnd + group.chipSpacingHorizontal
}
init()
}
override fun onDestroyView() {
super.onDestroyView()
viewModel.cleanup()
}
private fun init() {
val arguments = arguments
if (arguments != null) {
val fragmentArgs = UserSearchFragmentArgs.fromBundle(arguments)
actionLabel = fragmentArgs.actionLabel
title = fragmentArgs.title
multiple = fragmentArgs.multiple
viewModel.setHideThreadIds(fragmentArgs.hideThreadIds)
viewModel.setHideUserIds(fragmentArgs.hideUserIds)
viewModel.setSearchMode(fragmentArgs.searchMode)
viewModel.setShowGroups(fragmentArgs.showGroups)
}
setupTitles()
setupInput()
setupResults()
setupObservers()
// show cached results
viewModel.showCachedResults()
}
private fun setupTitles() {
if (!actionLabel.isNullOrBlank()) {
binding.done.text = actionLabel
}
if (title.isNullOrBlank()) return
(activity as MainActivity?)?.supportActionBar?.title = title
}
private fun setupResults() {
val context = context ?: return
binding.results.layoutManager = LinearLayoutManager(context)
resultsAdapter = UserSearchResultsAdapter(multiple) { _: Int, recipient: RankedRecipient, selected: Boolean ->
if (!multiple) {
val navController = NavHostFragment.findNavController(this)
if (!setResult(navController, recipient)) return@UserSearchResultsAdapter
navController.navigateUp()
return@UserSearchResultsAdapter
}
viewModel.setSelectedRecipient(recipient, !selected)
resultsAdapter?.setSelectedRecipient(recipient, !selected)
if (!selected) {
createChip(recipient)
return@UserSearchResultsAdapter
}
val chip = findChip(recipient) ?: return@UserSearchResultsAdapter
removeChipFromGroup(chip)
}
binding.results.adapter = resultsAdapter
binding.done.setOnClickListener {
val navController = NavHostFragment.findNavController(this)
if (!setResult(navController, viewModel.selectedRecipients)) return@setOnClickListener
navController.navigateUp()
}
}
private fun setResult(navController: NavController, rankedRecipient: RankedRecipient): Boolean {
navController.previousBackStackEntry?.savedStateHandle?.set("result", rankedRecipient) ?: return false
return true
}
private fun setResult(navController: NavController, rankedRecipients: Set<RankedRecipient>): Boolean {
navController.previousBackStackEntry?.savedStateHandle?.set("result", rankedRecipients) ?: return false
return true
}
private fun setupInput() {
binding.search.addTextChangedListener(object : TextWatcherAdapter() {
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
viewModel.search(s.toString().trimAll())
}
})
binding.search.setOnKeyListener { _: View?, _: Int, event: KeyEvent? ->
if (event != null && event.action == KeyEvent.ACTION_DOWN && event.keyCode == KeyEvent.KEYCODE_DEL) {
val chip = lastChip ?: return@setOnKeyListener false
removeChip(chip)
}
false
}
binding.group.setOnHierarchyChangeListener(object : OnHierarchyChangeListener {
override fun onChildViewAdded(parent: View, child: View) {}
override fun onChildViewRemoved(parent: View, child: View) {
binding.group.post {
TransitionManager.beginDelayedTransition(binding.root)
calculateInputWidth(0)
}
}
})
}
private fun setupObservers() {
viewModel.recipients.observe(viewLifecycleOwner) {
if (it == null) return@observe
when (it.status) {
Resource.Status.SUCCESS -> if (it.data != null) {
resultsAdapter?.submitList(it.data)
}
Resource.Status.ERROR -> {
if (it.message != null) {
Snackbar.make(binding.root, it.message, Snackbar.LENGTH_LONG).show()
}
if (it.resId != 0) {
Snackbar.make(binding.root, it.resId, Snackbar.LENGTH_LONG).show()
}
if (it.data != null) {
resultsAdapter?.submitList(it.data)
}
}
Resource.Status.LOADING -> if (it.data != null) {
resultsAdapter?.submitList(it.data)
}
}
}
viewModel.showAction().observe(viewLifecycleOwner) { binding.done.visibility = if (it) View.VISIBLE else View.GONE }
}
private fun createChip(recipient: RankedRecipient) {
val context = context ?: return
val chip = Chip(context).apply {
tag = recipient
text = getRecipientText(recipient)
isCloseIconVisible = true
setOnCloseIconClickListener { removeChip(this) }
}
binding.group.post {
val measure = measure(chip, binding.group)
TransitionManager.beginDelayedTransition(binding.root)
calculateInputWidth(if (measure.second != null) measure.second else 0)
binding.group.addView(chip, binding.group.childCount - 1)
}
}
private fun getRecipientText(recipient: RankedRecipient?): String? = when {
recipient == null -> null
recipient.user != null -> recipient.user.fullName
recipient.thread != null -> recipient.thread.threadTitle
else -> null
}
private fun removeChip(chip: View) {
val recipient = chip.tag as RankedRecipient
viewModel.setSelectedRecipient(recipient, false)
resultsAdapter?.setSelectedRecipient(recipient, false)
removeChipFromGroup(chip)
}
private fun findChip(recipient: RankedRecipient?): View? {
if (recipient == null || recipient.user == null && recipient.thread == null) return null
val isUser = recipient.user != null
val childCount = binding.group.childCount
if (childCount == 0) return null
for (i in childCount - 1 downTo 0) {
val child = binding.group.getChildAt(i) ?: continue
val tag = child.tag as RankedRecipient
if (isUser && tag.user == null || !isUser && tag.thread == null) continue
if (isUser && tag.user?.pk == recipient.user?.pk || !isUser && tag.thread?.threadId == recipient.thread?.threadId) {
return child
}
}
return null
}
private fun removeChipFromGroup(chip: View) {
binding.group.post {
TransitionManager.beginDelayedTransition(binding.root)
binding.group.removeView(chip)
}
}
private fun calculateInputWidth(newChipWidth: Int) {
var lastRight = lastChip?.right ?: 0
val remainingSpaceInRow = windowWidth - lastRight
if (remainingSpaceInRow < newChipWidth) {
// next chip will go to the next row, so assume no chips present
lastRight = 0
}
val newRight = lastRight + newChipWidth
val newInputWidth = windowWidth - newRight - paddingOffset
binding.search.layoutParams.width = if (newInputWidth < minInputWidth) windowWidth else newInputWidth
binding.search.requestLayout()
}
private val lastChip: View?
get() {
val childCount = binding.group.childCount
if (childCount == 0) return null
for (i in childCount - 1 downTo 0) {
val child = binding.group.getChildAt(i)
if (child is Chip) {
return child
}
}
return null
}
}

View File

@ -1,6 +1,6 @@
package awais.instagrabber.fragments
enum class UserSearchMode(name: String) {
enum class UserSearchMode(val mode: String) {
USER_SEARCH("user_name"),
RAVEN("raven"),
RESHARE("reshare");

View File

@ -192,7 +192,7 @@ public class UserSearchViewModel extends ViewModel {
private void rankedRecipientSearch() {
directMessagesRepository.rankedRecipients(
searchMode.name(),
searchMode.getMode(),
showGroups,
currentQuery,
CoroutineUtilsKt.getContinuation((response, throwable) -> {