zhanghai / AndroidFastScroll

Fast scroll for Android RecyclerView and more
https://play.google.com/store/apps/details?id=me.zhanghai.android.fastscroll.sample
Apache License 2.0
705 stars 67 forks source link

滚动条会乱跳 #57

Closed qianqianNotqianqian closed 6 months ago

qianqianNotqianqian commented 6 months ago

在配合如下代码会出现这种情况:package mapleleaf.materialdesign.engine.ui.fragments

import android.animation.ObjectAnimator import android.annotation.SuppressLint import android.content.ActivityNotFoundException import android.content.ComponentName import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.os.Bundle import android.text.Editable import android.text.Spannable import android.text.SpannableString import android.text.style.ForegroundColorSpan import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.widget.Button import android.widget.EditText import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.google.android.material.card.MaterialCardView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import mapleleaf.materialdesign.engine.R import mapleleaf.materialdesign.engine.base.UniversalFragmentBase import mapleleaf.materialdesign.engine.ui.dialog.DialogHelper import mapleleaf.materialdesign.engine.utils.SearchTextWatcher import mapleleaf.materialdesign.engine.utils.toast import me.zhanghai.android.fastscroll.FastScrollerBuilder import java.util.regex.Pattern

class FragmentComponentActivities : UniversalFragmentBase() {

private lateinit var recyclerView: RecyclerView
private lateinit var adapter: AdapterComponents
private lateinit var swipeRefreshLayout: SwipeRefreshLayout

override val layoutResId: Int
    get() = R.layout.fragment_components

override fun onViewCreated(rootView: View, savedInstanceState: Bundle?) {
    super.onViewCreated(rootView, savedInstanceState)

    val packageName = arguments?.getString("packageName") ?: return

    val imageView = rootView.findViewById<ImageView>(R.id.null_list)
    val appsSearchBox = rootView.findViewById<EditText>(R.id.apps_search_box)

    recyclerView = rootView.findViewById(R.id.recyclerView)

    swipeRefreshLayout = rootView.findViewById(R.id.swipeRefreshLayout)
    recyclerView.layoutManager =
        LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
    adapter = AdapterComponents(requireContext(), emptyList())
    recyclerView.adapter = adapter
    FastScrollerBuilder(recyclerView).build()

    val colorRed = ContextCompat.getColor(requireContext(), R.color.red1)
    val colorGreen = ContextCompat.getColor(requireContext(), R.color.lawngreen)
    val colorBlue = ContextCompat.getColor(requireContext(), R.color.blue)
    val colorOrange = ContextCompat.getColor(requireContext(), R.color.orange2)
    val progressColors =
        ContextCompat.getColor(requireContext(), R.color.swipe_refresh_layout_progress)
    swipeRefreshLayout.setColorSchemeColors(colorRed, colorGreen, colorBlue, colorOrange)
    swipeRefreshLayout.setProgressBackgroundColorSchemeColor(progressColors)

    lifecycleScope.launch(Dispatchers.Main) {
        val activities = withContext(Dispatchers.IO) {
            getActivities(packageName)
        }

        val visibility = if (activities.isEmpty()) View.GONE else View.VISIBLE
        adapter.setData(activities.toList())
        recyclerView.adapter = adapter
        recyclerView.visibility = visibility
        appsSearchBox.visibility = visibility
        swipeRefreshLayout.visibility = visibility
        imageView.visibility = if (visibility == View.VISIBLE) View.GONE else View.VISIBLE

        val defaultSearchText = appsSearchBox.text
        setupSearchBox(appsSearchBox)
        searchApp(defaultSearchText)
    }

    swipeRefreshLayout.setOnRefreshListener {
        lifecycleScope.launch(Dispatchers.Main) {
            if (::adapter.isInitialized) {
                val searchQuery = appsSearchBox.text.toString()
                if (searchQuery.isNotBlank()) {
                    searchApp(appsSearchBox.text)
                    swipeRefreshLayout.isRefreshing = false
                } else {
                    val activities = withContext(Dispatchers.IO) {
                        getActivities(packageName)
                    }
                    adapter.setData(activities.toList())
                    swipeRefreshLayout.isRefreshing = false
                }
            }
        }
    }
}

private fun setupSearchBox(appsSearchBox: EditText) {
    appsSearchBox.setOnEditorActionListener { _, actionId, _ ->
        if (actionId == EditorInfo.IME_ACTION_DONE || actionId == EditorInfo.IME_ACTION_NEXT || actionId == EditorInfo.IME_ACTION_SEARCH) {
            searchApp(appsSearchBox.text)
        }
        true
    }

    val searchTextWatcher = SearchTextWatcher {
        searchApp(appsSearchBox.text)
    }
    appsSearchBox.addTextChangedListener(searchTextWatcher)
}

private fun searchApp(text: Editable?) {
    val searchText = text?.toString() ?: ""
    if (searchText.isNotEmpty()) {
        val filteredProviders = getFilteredActivities(searchText)
        adapter.setData(filteredProviders)
    } else {
        val packageName = arguments?.getString("packageName") ?: return
        val providers = getActivities(packageName)
        adapter.setData(providers.toList())
    }

    adapter.setSearchText(searchText)
}

private fun getFilteredActivities(searchText: String): List<ResolveInfo> {
    val packageName = arguments?.getString("packageName") ?: return emptyList()
    val allActivities = getActivities(packageName)
    val packageManager = requireContext().packageManager
    return allActivities.filter { activity ->
        val activityName = activity.activityInfo.name
        val activityLabel = activity.loadLabel(packageManager).toString()
        activityName.contains(searchText, ignoreCase = true) || activityLabel.contains(
            searchText,
            ignoreCase = true
        )
    }
}

private fun getActivities(packageName: String): List<ResolveInfo> {
    return try {
        val packageInfo = requireContext().packageManager.getPackageInfo(
            packageName, PackageManager.GET_ACTIVITIES
        )
        packageInfo.activities?.map { activity ->
            ResolveInfo().apply {
                this.activityInfo = activity
            }
        }?.sortedBy { it.activityInfo.name } ?: emptyList()
    } catch (e: PackageManager.NameNotFoundException) {
        e.printStackTrace()
        emptyList()
    }
}

class AdapterComponents(
    private val context: Context,
    var activityList: List<ResolveInfo>,
) : RecyclerView.Adapter<AdapterComponents.ViewHolder>() {

    private var searchText: String = ""

// private var diffCallback: DiffCallback? = null

    @SuppressLint("NotifyDataSetChanged")
    fun setSearchText(text: String) {
        searchText = text
        notifyDataSetChanged()
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.list_item_component, parent, false)
        return ViewHolder(view)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val resolveInfo = activityList[position]
        holder.bind(resolveInfo)
    }

    override fun getItemCount(): Int {
        return activityList.size
    }

    @SuppressLint("NotifyDataSetChanged")
    fun setData(newData: List<ResolveInfo>) {

// diffCallback?.let { // val result = DiffUtil.calculateDiff(it) // activityList = newData // result.dispatchUpdatesTo(this) // } // diffCallback = DiffCallback(activityList, newData) activityList = newData notifyDataSetChanged() }

    inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView),
        View.OnClickListener {
        private val activityIcon: ImageView = itemView.findViewById(R.id.ItemIcon)
        private val activityLabelTextView: TextView = itemView.findViewById(R.id.ItemTitle)
        private val activityNameTextView: TextView = itemView.findViewById(R.id.ItemText)
        private val componentMaterialCardView: MaterialCardView =
            itemView.findViewById(R.id.componentMaterialCardView)

        init {
            componentMaterialCardView.setOnClickListener(this)
        }

        @SuppressLint("InflateParams")
        override fun onClick(v: View?) {
            val activitiesInfo = activityList[bindingAdapterPosition]
            val layoutInflater =
                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
            val dialogView = layoutInflater.inflate(R.layout.dialog_components_detail, null)
            val dialog = DialogHelper.customDialog(context, dialogView)

            dialogView.apply {
                findViewById<Button>(R.id.action_start).isVisible = true
                findViewById<LinearLayout>(R.id.activity_full_name).isVisible = true
                findViewById<LinearLayout>(R.id.package_full_name).isVisible = true
                findViewById<LinearLayout>(R.id.component_full_name).isVisible = false
            }

            fun setTextAndColor(textView: TextView, value: Boolean) {
                textView.text = value.toString()
                textView.setTextColor(
                    ContextCompat.getColor(
                        context,
                        if (value) R.color.green else R.color.red
                    )
                )
            }

            val enabledTextView = dialogView.findViewById<TextView>(R.id.state_enable)
            setTextAndColor(enabledTextView, activitiesInfo.activityInfo.enabled)

            val exportedTextView = dialogView.findViewById<TextView>(R.id.state_exported)
            setTextAndColor(exportedTextView, activitiesInfo.activityInfo.exported)

            dialogView.findViewById<ImageView>(R.id.imageView_icon)
                .setImageDrawable(activitiesInfo.activityInfo.loadIcon(context.packageManager))

// Glide.with(context) // .load(activitiesInfo.activityInfo.loadIcon(context.packageManager)) // .error(R.drawable.ic_error) // .diskCacheStrategy(DiskCacheStrategy.ALL) // .transition(DrawableTransitionOptions.withCrossFade(300)) // .into(imageViewIcon)

            fun setTextToEditText(editText: EditText, text: String) {
                editText.setText(text)
            }
            setTextToEditText(
                dialogView.findViewById(R.id.edit_title),
                activitiesInfo.activityInfo.loadLabel(context.packageManager).toString()
            )
            setTextToEditText(
                dialogView.findViewById(R.id.edit_pkg),
                activitiesInfo.activityInfo.packageName
            )
            setTextToEditText(
                dialogView.findViewById(R.id.edit_activity),
                activitiesInfo.activityInfo.name
            )

            dialogView.findViewById<View>(R.id.btn_cancel)
                .setOnClickListener { dialog.dismiss() }
            dialogView.findViewById<View>(R.id.action_start).setOnClickListener {
                dialog.dismiss()
                if (activitiesInfo.activityInfo.exported) {
                    val intent = Intent().apply {
                        component = ComponentName(
                            activitiesInfo.activityInfo.packageName,
                            activitiesInfo.activityInfo.name
                        )
                        addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                    }
                    try {
                        context.startActivity(intent)
                    } catch (e: ActivityNotFoundException) {
                        toast("启动 Activity 失败")
                    } catch (e: SecurityException) {
                        toast("您没有权限运行此 Activity")
                    }
                } else {
                    try {
                        // 构建 shell 命令
                        val command =
                            "am start -n ${activitiesInfo.activityInfo.packageName}/${activitiesInfo.activityInfo.name}"
                        // 获取 root 权限并执行命令
                        Runtime.getRuntime().exec(arrayOf("su", "-c", command))
                    } catch (e: SecurityException) {
                        toast("您没有权限运行此 Activity")
                    } catch (e: Exception) {
                        // 处理其他异常情况
                        toast("启动 Activity 失败")
                    }
                }
            }
        }

        fun bind(resolveInfo: ResolveInfo) {
            val activityLabel = resolveInfo.loadLabel(context.packageManager).toString()
            val activityName = resolveInfo.activityInfo.name
            if (!resolveInfo.activityInfo.exported) {
                activityNameTextView.setTextColor(ContextCompat.getColor(context, R.color.red))
            } else {
                activityNameTextView.setTextColor(
                    ContextCompat.getColor(
                        context,
                        R.color.green
                    )
                )
            }
            activityNameTextView.text = activityName.highlightText(searchText)
            activityLabelTextView.text = activityLabel.highlightText(searchText)

            Glide.with(context)
                .load(resolveInfo.activityInfo.loadIcon(context.packageManager))
                .error(R.drawable.ic_error)
                .diskCacheStrategy(DiskCacheStrategy.ALL)
                .transition(DrawableTransitionOptions.withCrossFade(300))
                .into(activityIcon)
        }
    }
}

} 当条目高度不一时,就会乱跳,而且当我拖动滚动条就会因跳动导致整个列表都乱跳

qianqianNotqianqian commented 6 months ago

这种情况仅在和recycleview绑定时出现

zhanghai commented 6 months ago

默认不支持高度不一致的条目,请仔细阅读 README。