[WIP] UI refactor
This commit is contained in:
15 files changed
+227
-75
No files matched your search
@@ -1,16 +1,19 @@
|
||||
package com.github.kr328.clash
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.core.view.isEmpty
|
||||
import android.util.DisplayMetrics
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.LinearSmoothScroller
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.kr328.clash.adapter.AbstractProxyAdapter
|
||||
import com.github.kr328.clash.adapter.GridProxyAdapter
|
||||
import com.github.kr328.clash.core.model.ProxyGroup
|
||||
import com.github.kr328.clash.core.utils.Log
|
||||
import com.github.kr328.clash.adapter.ProxyChipAdapter
|
||||
import com.github.kr328.clash.remote.withClash
|
||||
import com.github.kr328.clash.utils.ProxySorter
|
||||
import com.google.android.material.chip.Chip
|
||||
import kotlinx.android.synthetic.main.activity_proxies.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@@ -18,6 +21,7 @@ class ProxiesActivity : BaseActivity() {
|
||||
private val activity: ProxiesActivity
|
||||
get() = this
|
||||
private var maskedGroup: MutableSet<String> = mutableSetOf()
|
||||
private val updateChipChannel = Channel<Unit>(Channel.CONFLATED)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -34,6 +38,58 @@ class ProxiesActivity : BaseActivity() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mainList.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
updateChipChannel.offer(Unit)
|
||||
}
|
||||
})
|
||||
|
||||
chipList.adapter = ProxyChipAdapter(this) {
|
||||
launch {
|
||||
(mainList.adapter!! as AbstractProxyAdapter).getGroupPosition(it)?.also {
|
||||
val scroller = (object : LinearSmoothScroller(activity) {
|
||||
override fun getVerticalSnapPreference(): Int {
|
||||
return SNAP_TO_START
|
||||
}
|
||||
|
||||
override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics?): Float {
|
||||
return 40f / displayMetrics!!.densityDpi
|
||||
}
|
||||
|
||||
init {
|
||||
targetPosition = it
|
||||
}
|
||||
})
|
||||
|
||||
mainList.layoutManager?.startSmoothScroll(scroller)
|
||||
}
|
||||
}
|
||||
}
|
||||
chipList.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
|
||||
chipList.itemAnimator?.changeDuration = 80
|
||||
|
||||
launch {
|
||||
var currentChecked = ""
|
||||
|
||||
while (isActive) {
|
||||
updateChipChannel.receive()
|
||||
|
||||
val currentGroup = (mainList.adapter!! as AbstractProxyAdapter).getCurrentGroup()
|
||||
|
||||
if (currentChecked == currentGroup)
|
||||
continue
|
||||
|
||||
currentChecked = currentGroup
|
||||
|
||||
(chipList.adapter!! as ProxyChipAdapter).apply {
|
||||
selected = currentChecked
|
||||
|
||||
(chipList.layoutManager!! as LinearLayoutManager)
|
||||
.scrollToPositionWithOffset(chips.indexOf(currentChecked), 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
@@ -58,45 +114,13 @@ class ProxiesActivity : BaseActivity() {
|
||||
sorter.sort(proxies.toList())
|
||||
}
|
||||
|
||||
if (chipGroup.isEmpty()) {
|
||||
filtered.map(ProxyGroup::name).forEach {
|
||||
val chip = Chip(activity).apply {
|
||||
text = it
|
||||
|
||||
chipBackgroundColor = getColorStateList(R.color.proxies_chip_colors)
|
||||
rippleColor = getColorStateList(R.color.proxies_chip_colors)
|
||||
setTextColor(getColorStateList(R.color.proxies_chip_text_colors))
|
||||
checkedIcon = null
|
||||
|
||||
isCheckable = true
|
||||
isClickable = true
|
||||
isFocusable = true
|
||||
isChecked = !maskedGroup.contains(it)
|
||||
|
||||
setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked) {
|
||||
maskedGroup.remove(it)
|
||||
} else {
|
||||
maskedGroup.add(it)
|
||||
}
|
||||
|
||||
refreshList(it)
|
||||
}
|
||||
}
|
||||
|
||||
chipGroup.addView(chip)
|
||||
}
|
||||
}
|
||||
|
||||
(mainList.adapter!! as AbstractProxyAdapter).apply {
|
||||
root = filtered.filter { !maskedGroup.contains(it.name) }
|
||||
|
||||
applyChange()
|
||||
|
||||
if (scrollTo != null) {
|
||||
scrollToGroup(scrollTo)
|
||||
}
|
||||
}
|
||||
|
||||
(chipList.adapter!! as ProxyChipAdapter).chips = filtered.map { it.name }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,5 +7,6 @@ interface AbstractProxyAdapter {
|
||||
var onSelectProxyListener: suspend (String, String) -> Unit
|
||||
|
||||
suspend fun applyChange()
|
||||
suspend fun scrollToGroup(name: String)
|
||||
suspend fun getGroupPosition(name: String): Int?
|
||||
suspend fun getCurrentGroup(): String
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import com.github.kr328.clash.ProxiesActivity
|
||||
import com.github.kr328.clash.R
|
||||
import com.github.kr328.clash.core.model.Proxy
|
||||
import com.github.kr328.clash.core.model.ProxyGroup
|
||||
import com.github.kr328.clash.core.utils.Log
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -25,13 +24,18 @@ class GridProxyAdapter(private val context: ProxiesActivity, spanCount: Int = 2)
|
||||
RecyclerView.Adapter<RecyclerView.ViewHolder>(), AbstractProxyAdapter {
|
||||
private interface RenderInfo {
|
||||
val name: String
|
||||
val group: String
|
||||
}
|
||||
|
||||
private data class ProxyGroupInfo(override val name: String) : RenderInfo {
|
||||
override val group: String
|
||||
get() = name
|
||||
}
|
||||
|
||||
private data class ProxyGroupInfo(override val name: String) : RenderInfo
|
||||
private data class ProxyInfo(
|
||||
override val name: String,
|
||||
val type: Proxy.Type,
|
||||
val group: String,
|
||||
override val group: String,
|
||||
val selectable: Boolean,
|
||||
val delay: Short,
|
||||
val active: Boolean
|
||||
@@ -115,17 +119,24 @@ class GridProxyAdapter(private val context: ProxiesActivity, spanCount: Int = 2)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun scrollToGroup(name: String) {
|
||||
val position = withContext(Dispatchers.Default) {
|
||||
override suspend fun getGroupPosition(name: String): Int? {
|
||||
return withContext(Dispatchers.Default) {
|
||||
renderList.mapIndexed { index, p ->
|
||||
if ( p is ProxyGroupInfo && p.name == name )
|
||||
if (p is ProxyGroupInfo && p.name == name)
|
||||
index
|
||||
else
|
||||
-1
|
||||
}.singleOrNull { it >= 0 }
|
||||
} ?: return
|
||||
}
|
||||
}
|
||||
|
||||
layoutManager.scrollToPositionWithOffset(position, 0)
|
||||
override suspend fun getCurrentGroup(): String {
|
||||
val position = layoutManager.findFirstCompletelyVisibleItemPosition()
|
||||
|
||||
if (position < 0)
|
||||
return ""
|
||||
|
||||
return renderList[position].group
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.github.kr328.clash.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.kr328.clash.R
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
|
||||
class ProxyChipAdapter(
|
||||
private val context: Context,
|
||||
private val onClick: (String) -> Unit
|
||||
) :
|
||||
RecyclerView.Adapter<ProxyChipAdapter.Holder>() {
|
||||
var chips = listOf<String>()
|
||||
var selected: String = ""
|
||||
set(value) {
|
||||
val lastIndex = chips.indexOf(field)
|
||||
val newIndex = chips.indexOf(value)
|
||||
|
||||
field = value
|
||||
|
||||
if (lastIndex >= 0)
|
||||
notifyItemChanged(lastIndex)
|
||||
if (newIndex >= 0)
|
||||
notifyItemChanged(newIndex)
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
private val colorOnSurface: Int
|
||||
|
||||
init {
|
||||
val typedValue = TypedValue()
|
||||
|
||||
context.theme.resolveAttribute(R.attr.colorOnSurface, typedValue, true)
|
||||
colorOnSurface = typedValue.data
|
||||
}
|
||||
|
||||
class Holder(root: View) : RecyclerView.ViewHolder(root) {
|
||||
val card: MaterialCardView = root.findViewById(R.id.root)
|
||||
val title: TextView = root.findViewById(android.R.id.title)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
|
||||
val layoutInflater = LayoutInflater.from(context)
|
||||
|
||||
return Holder(layoutInflater.inflate(R.layout.adapter_proxies_chip, parent, false))
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return chips.size
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: Holder, position: Int) {
|
||||
val current = chips[position]
|
||||
|
||||
holder.title.text = current
|
||||
holder.card.setOnClickListener {
|
||||
onClick(current)
|
||||
}
|
||||
|
||||
if (selected == current) {
|
||||
holder.title.setTextColor(Color.WHITE)
|
||||
holder.card.setCardBackgroundColor(context.getColor(R.color.primaryCardColorStarted))
|
||||
} else {
|
||||
holder.title.setTextColor(colorOnSurface)
|
||||
holder.card.setCardBackgroundColor(context.getColor(R.color.chipBackgroundColor))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<alpha android:fromAlpha="1.0" android:toAlpha="0.5" />
|
||||
<alpha android:fromAlpha="0.5" android:toAlpha="1.0" />
|
||||
</set>
|
||||
@@ -17,20 +17,11 @@
|
||||
android:layout_height="wrap_content" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<HorizontalScrollView
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/chipList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:scrollbars="none"
|
||||
android:overScrollMode="never">
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/chipGroup"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:animateLayoutChanges="true"
|
||||
android:paddingStart="15dp"
|
||||
android:paddingEnd="15dp" />
|
||||
</HorizontalScrollView>
|
||||
android:layout_marginBottom="10dp"/>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/mainList"
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/root"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginTop="10dp"
|
||||
app:cardCornerRadius="12dp"
|
||||
android:focusable="true"
|
||||
android:clickable="true">
|
||||
|
||||
<TextView
|
||||
android:id="@android:id/title"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body2"
|
||||
android:minEms="3"
|
||||
android:gravity="center"/>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
@@ -74,6 +74,14 @@
|
||||
<string name="new_profile">New Profile</string>
|
||||
<string name="edit_profile">Edit Profile</string>
|
||||
<string name="clash_start_failure">Clash Start Failure</string>
|
||||
<string name="refresh">Refresh</string>
|
||||
<string name="group_order">Group Order</string>
|
||||
<string name="proxy_order">Proxy Order</string>
|
||||
<string name="default_">Default</string>
|
||||
<string name="delay">Delay</string>
|
||||
<string name="direct">Direct</string>
|
||||
<string name="global">Global</string>
|
||||
<string name="rule">Rule</string>
|
||||
|
||||
<string name="start_url_provider_failure">Start URL Provider Failure</string>
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.github.kr328.clash.core
|
||||
|
||||
import android.content.Context
|
||||
import bridge.Bridge
|
||||
import bridge.EventPoll
|
||||
import com.github.kr328.clash.core.event.EventStream
|
||||
import com.github.kr328.clash.core.event.LogEvent
|
||||
import com.github.kr328.clash.core.model.General
|
||||
@@ -15,7 +14,6 @@ import com.github.kr328.clash.core.transact.ProxyGroupCollectionImpl
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
object Clash {
|
||||
private var initialized = false
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
package com.github.kr328.clash.core.model
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import com.github.kr328.clash.core.serialization.MergedParcels
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
|
||||
@@ -6,7 +6,7 @@ import com.github.kr328.clash.core.serialization.MergedParcels
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ProxyGroupList(val list: List<ProxyGroup>): Parcelable {
|
||||
data class ProxyGroupList(val list: List<ProxyGroup>) : Parcelable {
|
||||
override fun writeToParcel(parcel: Parcel, flags: Int) {
|
||||
MergedParcels.dump(serializer(), this, parcel)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.github.kr328.clash.core.serialization
|
||||
|
||||
import android.os.Parcel
|
||||
import com.github.kr328.clash.core.utils.Log
|
||||
import kotlinx.serialization.*
|
||||
import kotlinx.serialization.modules.EmptyModule
|
||||
import kotlinx.serialization.modules.SerialModule
|
||||
@@ -18,8 +17,6 @@ object MergedParcels : AbstractSerialFormat(EmptyModule) {
|
||||
|
||||
parcel.writeStringList(encoder.getStringList())
|
||||
parcel.appendFrom(data, 0, data.dataSize())
|
||||
|
||||
Log.i("Send ${parcel.dataSize()} bytes")
|
||||
} finally {
|
||||
data.recycle()
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.github.kr328.clash.core.transact
|
||||
|
||||
import bridge.DoneCallback
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
class DoneCallbackImpl : DoneCallback, CompletableDeferred<Unit> by CompletableDeferred() {
|
||||
override fun doneWithError(e: Exception?) {
|
||||
|
||||
@@ -3,10 +3,21 @@ package com.github.kr328.clash.core.utils
|
||||
import com.github.kr328.clash.core.Constants.TAG
|
||||
|
||||
object Log {
|
||||
fun i(message: String, throwable: Throwable? = null) = android.util.Log.i(TAG, message, throwable)
|
||||
fun w(message: String, throwable: Throwable? = null) = android.util.Log.w(TAG, message, throwable)
|
||||
fun e(message: String, throwable: Throwable? = null) = android.util.Log.e(TAG, message, throwable)
|
||||
fun d(message: String, throwable: Throwable? = null) = android.util.Log.d(TAG, message, throwable)
|
||||
fun v(message: String, throwable: Throwable? = null) = android.util.Log.v(TAG, message, throwable)
|
||||
fun wtf(message: String, throwable: Throwable? = null) = android.util.Log.wtf(TAG, message, throwable)
|
||||
fun i(message: String, throwable: Throwable? = null) =
|
||||
android.util.Log.i(TAG, message, throwable)
|
||||
|
||||
fun w(message: String, throwable: Throwable? = null) =
|
||||
android.util.Log.w(TAG, message, throwable)
|
||||
|
||||
fun e(message: String, throwable: Throwable? = null) =
|
||||
android.util.Log.e(TAG, message, throwable)
|
||||
|
||||
fun d(message: String, throwable: Throwable? = null) =
|
||||
android.util.Log.d(TAG, message, throwable)
|
||||
|
||||
fun v(message: String, throwable: Throwable? = null) =
|
||||
android.util.Log.v(TAG, message, throwable)
|
||||
|
||||
fun wtf(message: String, throwable: Throwable? = null) =
|
||||
android.util.Log.wtf(TAG, message, throwable)
|
||||
}
|
||||
Reference in new issue
Block a user