use custom view

This commit is contained in:
Kr328 committed 2019-11-23 14:06:10 +08:00
1 parent 7270f94930
commit 3b03624f32
20 files changed
+396 -187

No files matched your search

+2
View File
@@ -30,5 +30,7 @@
<activity android:name=".ImportFileActivity"
android:label="@string/clash_import_from_file"
android:exported="false"/>
<activity android:name=".ClashStatusActivity"
android:exported="false" />
</application>
</manifest>
@@ -0,0 +1,7 @@
package com.github.kr328.clash
import androidx.appcompat.app.AppCompatActivity
class ClashStatusActivity : AppCompatActivity() {
}
@@ -12,6 +12,7 @@ import android.widget.ListView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.commit
import com.github.kr328.clash.view.FatItem
import kotlinx.android.synthetic.main.activity_new_profile.*
class CreateProfileActivity : AppCompatActivity() {
@@ -28,17 +29,19 @@ class CreateProfileActivity : AppCompatActivity() {
class Adapter(private val context: Context) : BaseAdapter() {
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
return (convertView ?: LayoutInflater.from(context).inflate(R.layout.adapter_new_profile, parent, false)).apply {
return ((convertView ?: FatItem(context)) as FatItem).apply {
val current = NEW_PROFILE_SOURCE[position]
findViewById<ImageView>(R.id.adapter_new_profile_icon).setImageResource(current.icon)
findViewById<TextView>(R.id.adapter_new_profile_title).text = context.getString(current.title)
findViewById<TextView>(R.id.adapter_new_profile_summary).text = context.getString(current.summary)
isClickable = false
icon = context.getDrawable(current.icon)
title = context.getString(current.title)
summary = context.getString(current.summary)
}
}
override fun getItem(position: Int): Any {
return NEW_PROFILE_SOURCE
return NEW_PROFILE_SOURCE[position]
}
override fun getItemId(position: Int): Long {
@@ -10,6 +10,7 @@ import android.net.VpnService
import android.os.Bundle
import android.os.Handler
import android.os.IBinder
import android.util.Log
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.MutableLiveData
@@ -96,7 +97,9 @@ class MainActivity : AppCompatActivity() {
activity_main_clash_status.setOnClickListener {
when ( clashStatus.value ?: ClashProcessStatus.STATUS_STOPPED ) {
ClashProcessStatus.STATUS_STARTED -> {}
ClashProcessStatus.STATUS_STARTED -> {
clash?.stop()
}
ClashProcessStatus.STATUS_STOPPED -> {
VpnService.prepare(this)?.apply {
startActivityForResult(this, VPN_REQUEST_CODE)
@@ -11,23 +11,19 @@ import androidx.appcompat.widget.AppCompatRadioButton
import androidx.recyclerview.widget.RecyclerView
import com.github.kr328.clash.R
import com.github.kr328.clash.service.data.ClashProfileEntity
import com.github.kr328.clash.view.RadioFatItem
class ProfileAdapter(private val context: Context, private val listener: (Int) -> Unit) :
RecyclerView.Adapter<ProfileAdapter.ProfileViewHolder>() {
var profiles: List<ClashProfileEntity> = emptyList()
class ProfileViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val clickable: View = view.findViewById(R.id.adapter_profiles_clickable)
val checked: AppCompatRadioButton = view.findViewById(R.id.adapter_profiles_checked)
val title: TextView = view.findViewById(R.id.adapter_profiles_title)
val summary: TextView = view.findViewById(R.id.adapter_profiles_summary)
val operation: ImageView = view.findViewById(R.id.adapter_profiles_operation_image)
}
class ProfileViewHolder(val view: RadioFatItem) : RecyclerView.ViewHolder(view)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProfileViewHolder {
return ProfileViewHolder(
LayoutInflater.from(context)
.inflate(R.layout.adapter_profile, parent, false)
RadioFatItem(context).apply {
layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
}
)
}
@@ -38,23 +34,23 @@ class ProfileAdapter(private val context: Context, private val listener: (Int) -
override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) {
val current = profiles[position]
holder.title.text = current.name
holder.summary.text = context.getString(
holder.view.title = current.name
holder.view.summary = context.getString(
R.string.clash_profile_item_summary,
current.proxies, current.proxyGroups, current.rules
)
holder.checked.isChecked = current.selected
holder.clickable.setOnClickListener {
holder.view.isChecked = current.selected
holder.view.setOnClickListener {
listener(current.id)
}
with(current.token) {
when {
startsWith("http") || startsWith("content") -> {
holder.operation.setImageResource(R.drawable.ic_profile_refresh)
holder.view.operation = context.getDrawable(R.drawable.ic_profile_refresh)
}
startsWith("file") -> {
holder.operation.setImageResource(R.drawable.ic_profile_edit)
holder.view.operation = context.getDrawable(R.drawable.ic_profile_edit)
}
}
}
@@ -1,18 +0,0 @@
package com.github.kr328.clash.view
import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import android.view.View
class FatButton @JvmOverloads constructor(context: Context, attributeSet: AttributeSet? = null, defaultStyleAttr: Int = 0, defaultStyleRes: Int = 0) :
View(context, attributeSet, defaultStyleAttr, defaultStyleRes) {
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
canvas?.apply {
}
}
}
@@ -0,0 +1,97 @@
package com.github.kr328.clash.view
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import android.widget.TextView
import com.github.kr328.clash.R
class FatItem @JvmOverloads constructor(context: Context, attributeSet: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0) :
FrameLayout(context, attributeSet, defStyleAttr, defStyleRes) {
private val titleView: TextView
private val summaryView: TextView
private val operationView: View
private val iconView: View
private val dividerView: View
private val clickable: View
private val operationClickable: View
var icon: Drawable?
get() = iconView.background
set(value) { iconView.background = value }
var title: CharSequence?
get() = titleView.text
set(value) { titleView.text = value }
var summary: CharSequence?
get() = summaryView.text
set(value) {
summaryView.text = value
if ( value?.isNotBlank() != true )
summaryView.visibility = View.GONE
else
summaryView.visibility = View.VISIBLE
}
var operation: Drawable?
get() = operationView.background
set(value) {
operationView.background = value
if ( value == null )
operationClickable.visibility = View.GONE
else
operationClickable.visibility = View.VISIBLE
}
override fun setOnClickListener(l: OnClickListener?) {
clickable.setOnClickListener(l)
}
override fun setOnLongClickListener(l: OnLongClickListener?) {
clickable.setOnLongClickListener(l)
}
fun setOperationOnClickListener(l: OnClickListener?) {
operationClickable.setOnClickListener(l)
}
fun setOperationOnLongClickListener(l: OnLongClickListener?) {
operationClickable.setOnLongClickListener(l)
}
override fun setClickable(clickable: Boolean) {
this.clickable.isFocusable = clickable
this.clickable.isClickable = clickable
this.operationClickable.isFocusable = clickable
this.operationClickable.isClickable = clickable
}
override fun isClickable(): Boolean {
return this.clickable.isClickable
}
init {
with ( LayoutInflater.from(context).inflate(R.layout.view_fat_item, this, true) ) {
titleView = findViewById(R.id.view_fat_item_title)
summaryView = findViewById(R.id.view_fat_item_summary)
operationView = findViewById(R.id.view_fat_item_operation)
dividerView = findViewById(R.id.view_fat_item_divider)
iconView = findViewById(R.id.view_fat_item_icon)
clickable = findViewById(R.id.view_fat_item_clickable)
operationClickable = findViewById(R.id.view_fat_item_operation_clickable)
}
context.theme.obtainStyledAttributes(attributeSet, R.styleable.FatItem, 0, 0).apply {
try {
icon = getDrawable(R.styleable.FatItem_icon)
title = getString(R.styleable.FatItem_title)
summary = getString(R.styleable.FatItem_summary)
operation = getDrawable(R.styleable.FatItem_operation)
}
finally {
recycle()
}
}
}
}
@@ -0,0 +1,86 @@
package com.github.kr328.clash.view
import android.content.Context
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.widget.FrameLayout
import android.widget.TextView
import com.github.kr328.clash.R
import com.google.android.material.radiobutton.MaterialRadioButton
class RadioFatItem @JvmOverloads constructor(context: Context, attributeSet: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0) :
FrameLayout(context, attributeSet, defStyleAttr, defStyleRes) {
private val titleView: TextView
private val summaryView: TextView
private val operationView: View
private val radioView: MaterialRadioButton
private val dividerView: View
private val clickable: View
private val operationClickable: View
var isChecked: Boolean
get() = radioView.isChecked
set(value) { radioView.isChecked = value }
var title: CharSequence?
get() = titleView.text
set(value) { titleView.text = value }
var summary: CharSequence?
get() = summaryView.text
set(value) {
summaryView.text = value
if ( value?.isNotBlank() != true )
summaryView.visibility = View.GONE
else
summaryView.visibility = View.VISIBLE
}
var operation: Drawable?
get() = operationView.background
set(value) {
operationView.background = value
if ( value == null )
operationClickable.visibility = View.GONE
else
operationClickable.visibility = View.VISIBLE
}
override fun setOnClickListener(l: OnClickListener?) {
clickable.setOnClickListener(l)
}
override fun setOnLongClickListener(l: OnLongClickListener?) {
clickable.setOnLongClickListener(l)
}
fun setOperationOnClickListener(l: OnClickListener?) {
operationClickable.setOnClickListener(l)
}
fun setOperationOnLongClickListener(l: OnLongClickListener?) {
operationClickable.setOnLongClickListener(l)
}
init {
with ( LayoutInflater.from(context).inflate(R.layout.view_radio_fat_item, this, true) ) {
titleView = findViewById(R.id.view_radio_fat_item_title)
summaryView = findViewById(R.id.view_radio_fat_item_summary)
operationView = findViewById(R.id.view_radio_fat_item_operation)
dividerView = findViewById(R.id.view_radio_fat_item_divider)
radioView = findViewById(R.id.view_radio_fat_item_radio)
clickable = findViewById(R.id.view_radio_fat_item_clickable)
operationClickable = findViewById(R.id.view_radio_fat_item_operation_clickable)
}
context.theme.obtainStyledAttributes(attributeSet, R.styleable.FatItem, 0, 0).apply {
try {
title = getString(R.styleable.FatItem_title)
summary = getString(R.styleable.FatItem_summary)
operation = getDrawable(R.styleable.FatItem_operation)
}
finally {
recycle()
}
}
}
}
@@ -40,7 +40,7 @@
<TextView
android:id="@+id/activity_main_clash_status_summary"
android:text="@string/clash_status_click_to_start"
android:textColor="@color/light_gray"
android:textColor="@color/lightGray"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:layout_marginTop="5dp"
android:layout_alignStart="@id/activity_main_clash_status_title"
+7 -25
View File
@@ -1,5 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
@@ -29,32 +31,12 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout
<com.github.kr328.clash.view.FatItem
android:id="@+id/activity_profiles_new_profile"
android:focusable="true"
android:clickable="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="70dp"
android:background="?attr/selectableItemBackground">
<ImageView
android:src="@drawable/ic_new_profile"
android:contentDescription="@string/app_name"
android:layout_marginStart="20dp"
android:layout_marginEnd="30dp"
android:layout_width="30dp"
android:layout_height="30dp" />
<TextView
android:text="@string/clash_new_profile"
android:textStyle="bold"
android:textSize="16sp"
android:gravity="center|start"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
android:layout_height="wrap_content"
app:icon="@drawable/ic_new_profile"
app:title="@string/clash_new_profile" />
</LinearLayout>
</ScrollView>
</LinearLayout>
@@ -1,44 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main_clash_profiles"
android:layout_marginTop="10dp"
android:layout_marginStart="30dp"
android:layout_marginEnd="30dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="18dp"
android:paddingEnd="18dp"
android:paddingTop="15dp"
android:paddingBottom="15dp"
app:cardCornerRadius="8dp"
app:cardBackgroundColor="@android:color/white">
<ImageView
android:id="@+id/adapter_new_profile_icon"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_centerVertical="true"
android:contentDescription="@string/app_name"
android:src="@drawable/ic_profiles" />
<TextView
android:id="@+id/adapter_new_profile_title"
android:layout_toEndOf="@id/adapter_new_profile_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="20dp"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:text="@string/clash_profiles" />
<TextView
android:id="@+id/adapter_new_profile_summary"
android:text="@string/clash_profiles_summary_unselected"
android:textColor="@android:color/darker_gray"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:layout_marginTop="2dp"
android:layout_alignStart="@id/adapter_new_profile_title"
android:layout_below="@id/adapter_new_profile_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</RelativeLayout>
@@ -1,70 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/adapter_profiles_clickable"
android:orientation="horizontal"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:focusable="true"
android:clickable="true"
android:background="?attr/selectableItemBackground">
<androidx.appcompat.widget.AppCompatRadioButton
android:id="@+id/adapter_profiles_checked"
android:focusable="false"
android:clickable="false"
android:layout_marginStart="20dp"
android:layout_width="60dp"
android:layout_height="80dp"/>
<LinearLayout
android:gravity="center_vertical"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="match_parent">
<TextView
android:id="@+id/adapter_profiles_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textStyle="bold"
android:textSize="16sp"
android:text="@string/app_name"/>
<TextView
android:id="@+id/adapter_profiles_summary"
android:textColor="@color/colorAccent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="end">
<View
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/light_gray"/>
<FrameLayout
android:id="@+id/adapter_profiles_operation"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:focusable="true"
android:clickable="true"
android:background="?attr/selectableItemBackground">
<ImageView
android:id="@+id/adapter_profiles_operation_image"
android:layout_margin="25dp"
android:contentDescription="@string/app_name"
android:layout_width="30dp"
android:layout_height="30dp" />
</FrameLayout>
</LinearLayout>
</LinearLayout>
+2 -1
View File
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
+68
View File
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/view_fat_item_clickable"
android:focusable="true"
android:clickable="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground">
<View
android:id="@+id/view_fat_item_icon"
android:layout_centerVertical="true"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_marginStart="27.5dp"/>
<LinearLayout
android:id="@+id/view_fat_item_text_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/view_fat_item_icon"
android:layout_toStartOf="@id/view_fat_item_operation_clickable"
android:layout_marginStart="27.5dp"
android:paddingTop="15dp"
android:paddingBottom="15dp"
android:orientation="vertical">
<TextView
android:id="@+id/view_fat_item_title"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_marginTop="5dp"
android:id="@+id/view_fat_item_summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
<LinearLayout
android:id="@+id/view_fat_item_operation_clickable"
android:layout_width="81dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:background="?attr/selectableItemBackground"
android:focusable="true"
android:clickable="true"
android:layout_alignParentEnd="true"
android:layout_alignTop="@id/view_fat_item_text_container"
android:layout_alignBottom="@id/view_fat_item_text_container">
<View
android:id="@+id/view_fat_item_divider"
android:layout_width="1dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:layout_height="wrap_content"
android:background="@color/lightGray"
android:layout_marginEnd="25dp" />
<View
android:id="@+id/view_fat_item_operation"
android:layout_marginEnd="25dp"
android:layout_width="30dp"
android:layout_height="30dp"/>
</LinearLayout>
</RelativeLayout>
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/view_radio_fat_item_clickable"
android:focusable="true"
android:clickable="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground">
<RadioButton
android:gravity="center"
android:clickable="false"
android:focusable="false"
android:id="@+id/view_radio_fat_item_radio"
android:layout_centerVertical="true"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginStart="24dp"/>
<LinearLayout
android:id="@+id/view_radio_fat_item_text_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_toStartOf="@id/view_radio_fat_item_operation_clickable"
android:layout_marginStart="80dp"
android:paddingTop="15dp"
android:paddingBottom="15dp"
android:orientation="vertical">
<TextView
android:id="@+id/view_radio_fat_item_title"
android:textAppearance="@style/TextAppearance.AppCompat.Medium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_marginTop="5dp"
android:id="@+id/view_radio_fat_item_summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
<LinearLayout
android:id="@+id/view_radio_fat_item_operation_clickable"
android:layout_width="81dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:focusable="true"
android:clickable="true"
android:background="?attr/selectableItemBackground"
android:layout_alignParentEnd="true"
android:layout_alignTop="@id/view_radio_fat_item_text_container"
android:layout_alignBottom="@id/view_radio_fat_item_text_container">
<View
android:id="@+id/view_radio_fat_item_divider"
android:layout_width="1dp"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:layout_height="wrap_content"
android:background="@color/lightGray"
android:layout_marginEnd="25dp" />
<View
android:id="@+id/view_radio_fat_item_operation"
android:layout_marginEnd="25dp"
android:layout_width="30dp"
android:layout_height="30dp"/>
</LinearLayout>
</RelativeLayout>
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="FatItem">
<attr name="icon" format="reference"/>
<attr name="title" format="string" />
<attr name="summary" format="string" />
<attr name="operation" format="string" />
</declare-styleable>
</resources>
+1 -1
View File
@@ -5,5 +5,5 @@
<color name="colorAccent">#1E4376</color>
<color name="ic_launcher_background">#FFFFFF</color>
<color name="light_gray">#d3d3d3</color>
<color name="lightGray">#d3d3d3</color>
</resources>
@@ -105,6 +105,8 @@ class TunService : VpnService() {
}
override fun onDestroy() {
super.onDestroy()
fileDescriptor.close()
clash.stopTunDevice()
@@ -113,8 +115,6 @@ class TunService : VpnService() {
unbindService(connection)
defaultNetworkObserver.unregister()
super.onDestroy()
}
private fun Builder.addDefaultDns(): Builder {
@@ -6,20 +6,27 @@ import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Handler
class DefaultNetworkObserver(val context: Context, val listener: (Network?) -> Unit) {
private val handler = Handler()
private val connectivity = context.getSystemService(ConnectivityManager::class.java)!!
private val active: Network?
get() = connectivity.activeNetwork
private var blocking: Boolean = true
private var current: Network? = null
private val callback = object: ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
if ( blocking )
return
current = network
listener(current)
}
override fun onLost(network: Network) {
if ( blocking )
return
if ( current == network )
current = null
@@ -30,6 +37,9 @@ class DefaultNetworkObserver(val context: Context, val listener: (Network?) -> U
network: Network,
networkCapabilities: NetworkCapabilities
) {
if ( blocking )
return
if ( network == current )
listener(current)
}
@@ -37,6 +47,12 @@ class DefaultNetworkObserver(val context: Context, val listener: (Network?) -> U
fun register() {
connectivity.registerNetworkCallback(NetworkRequest.Builder().build(), callback)
listener(connectivity.activeNetwork)
handler.postDelayed({
blocking = false
}, 1000)
}
fun unregister() {
+2 -2
View File
@@ -1,8 +1,8 @@
<resources>
<string name="app_name">Service</string>
<string name="app_name">Clash</string>
<string name="clash_service_status_channel">Clash Status</string>
<string name="clash_service_running">Clash Running</string>
<string name="clash_service_running">Running</string>
<string name="clash_service_running_message">Profile %s loaded</string>
<string name="clash_service_vpn_mode">VPN</string>
</resources>