? = null
31 |
32 | /**
33 | * Register underlying statistical provider.
34 | */
35 | fun registerProvider(provider: ITrackProvider) {
36 | providers.add(provider.apply {
37 | onInit()
38 | })
39 | }
40 |
41 | /**
42 | * Dispatch event without recursive node tree.
43 | */
44 | fun dispatchEvent(event: String, params: TrackParams) {
45 | for (provider in providers) {
46 | provider.onEvent(event, params)
47 | }
48 | }
49 | }
50 | }
51 |
52 | /**
53 | * @param eventName Event name.
54 | * @param otherParams Incoming event params
55 | *
56 | * @return Event Params around the node tree.
57 | */
58 | internal fun Any.doTrackEvent(eventName: String, otherParams: TrackParams? = null): TrackParams? {
59 | // 1. Check whether the underlying statistical provider are available.
60 | if (EasyTrack.providers.isEmpty()) {
61 | Log.d(TAG, "Try track event $eventName, but the providers is Empty.")
62 | return otherParams
63 | }
64 | // 2. Collect data recursively.
65 | val params = if (this is View || this is TrackModel) {
66 | fillTrackParams(this, otherParams)
67 | } else {
68 | otherParams
69 | }
70 | if (null == params) {
71 | return otherParams
72 | }
73 | // 3. Log.
74 | val logStrBuilder = if (EasyTrack.debug) {
75 | StringBuilder().apply {
76 | append(" ")
77 | append("\nonEvent:$eventName")
78 | for ((key, value) in params) {
79 | append("\n$key = $value")
80 | }
81 | }
82 | } else {
83 | null
84 | }
85 | // 4. Do event reporting.
86 | for (provider in EasyTrack.providers) {
87 | if (!provider.enabled) {
88 | logStrBuilder?.append("\nTry track event $eventName, but the provider is disabled.")
89 | continue
90 | }
91 | logStrBuilder?.append("\nTry track event $eventName with provider ${provider.name}.")
92 | provider.onEvent(eventName, params)
93 | }
94 | logStrBuilder?.append("\n------------------------------------------------------")?.also {
95 | Log.d(TAG, it.toString())
96 | }
97 | return params
98 | }
99 |
100 | /**
101 | * Collect data recursively
102 | */
103 | internal fun fillTrackParams(node: Any?, params: TrackParams? = null): TrackParams {
104 | val result = params ?: TrackParams()
105 | var curNode = node
106 | while (null != curNode) {
107 | when (curNode) {
108 | is View -> {
109 | if (android.R.id.content == curNode.id) {
110 | // View Root
111 | val activity = getActivityFromView(curNode)
112 | if (activity is IPageTrackNode) {
113 | // Activity node.
114 | activity.fillTrackParams(result)
115 | curNode = activity.referrerSnapshot()
116 | } else {
117 | curNode = null
118 | }
119 | } else {
120 | // View node
121 | curNode.trackModel?.fillTrackParams(result)
122 | curNode = curNode.parent
123 | }
124 | }
125 | is ITrackNode -> {
126 | // Track node
127 | curNode.fillTrackParams(result)
128 | curNode = curNode.parent
129 | }
130 | else -> {
131 | curNode = null
132 | }
133 | }
134 | }
135 | return result
136 | }
137 |
138 | ///**
139 | // * 创建 Event 实例
140 | // */
141 | //fun View?.newTrackEvent(eventName: String): TrackEvent {
142 | // this?.doTrackEvent(eventName)
143 | //}
144 |
145 | ///**
146 | // * 创建 Event 实例
147 | // */
148 | //fun ITrackModel?.newTrackEvent(eventName: String): TrackEvent {
149 | // this?.doTrackEvent(eventName)
150 | //}
--------------------------------------------------------------------------------
/easytrack/src/main/java/com/pengxr/easytrack/core/FillStrategy.kt:
--------------------------------------------------------------------------------
1 | package com.pengxr.easytrack.core
2 |
3 | import androidx.annotation.IntDef
4 | import com.pengxr.easytrack.core.FillStrategy.Companion.DEFAULT
5 | import com.pengxr.easytrack.core.FillStrategy.Companion.NON_CACHE
6 |
7 | /**
8 | * 数据填充策略
9 | *
10 | * Created by pengxr on 22/8/2021
11 | */
12 | @IntDef(DEFAULT, NON_CACHE)
13 | @Retention(AnnotationRetention.SOURCE)
14 | annotation class FillStrategy {
15 | companion object {
16 | const val DEFAULT = 0 // 缓存(默认的)
17 | const val NON_CACHE = 1 // 不缓存
18 | }
19 | }
--------------------------------------------------------------------------------
/easytrack/src/main/java/com/pengxr/easytrack/core/IPageTrackNode.kt:
--------------------------------------------------------------------------------
1 | package com.pengxr.easytrack.core
2 |
3 | /**
4 | * Created by pengxr on 10/9/2021
5 | */
6 | interface IPageTrackNode : ITrackModel {
7 |
8 | fun referrerKeyMap(): Map?
9 |
10 | fun referrerSnapshot(): ITrackNode?
11 | }
--------------------------------------------------------------------------------
/easytrack/src/main/java/com/pengxr/easytrack/core/ITrackModel.kt:
--------------------------------------------------------------------------------
1 | package com.pengxr.easytrack.core
2 |
3 | import java.io.Serializable
4 |
5 | /**
6 | * 定义数据埋点能力
7 | * Created by pengxr on 2021/8/18.
8 | */
9 | interface ITrackModel : Serializable {
10 |
11 | /**
12 | * 数据填充
13 | */
14 | fun fillTrackParams(params: TrackParams)
15 |
16 | // /**
17 | // * 启动埋点会话
18 | // */
19 | // @AnyThread
20 | // fun startSession(sessionName: String): TrackSession {
21 | // return TrackSession().apply {
22 | // sessionMap[sessionName] = this
23 | // }
24 | // }
25 | //
26 | // /**
27 | // * 移除埋点会话
28 | // */
29 | // @AnyThread
30 | // fun removeSession(sessionName: String) {
31 | // sessionMap[sessionName]?.isEnabled = false
32 | // sessionMap.remove(sessionName)
33 | // }
34 | //
35 | // /**
36 | // * 清除埋点会话
37 | // */
38 | // @AnyThread
39 | // fun clear() {
40 | // val oldSession = sessionMap
41 | // sessionMap = ConcurrentHashMap()
42 | //
43 | // for ((name, session) in oldSession) {
44 | // session.isEnabled = false
45 | // oldSession.remove(name)
46 | // }
47 | // }
48 | //
49 | // /**
50 | // * 获取埋点会话
51 | // */
52 | // @AnyThread
53 | // fun getSession(sessionName: String): TrackSession? = sessionMap[sessionName]
54 | }
--------------------------------------------------------------------------------
/easytrack/src/main/java/com/pengxr/easytrack/core/ITrackNode.kt:
--------------------------------------------------------------------------------
1 | package com.pengxr.easytrack.core
2 |
3 | /**
4 | * Created by pengxr on 10/9/2021
5 | */
6 | interface ITrackNode : ITrackModel {
7 |
8 | val parent: ITrackNode?
9 | }
--------------------------------------------------------------------------------
/easytrack/src/main/java/com/pengxr/easytrack/core/ITrackProvider.kt:
--------------------------------------------------------------------------------
1 | package com.pengxr.easytrack.core
2 |
3 | import androidx.annotation.UiThread
4 |
5 | /**
6 | * Underlying statistical provider.
7 | *
8 | * Created by pengxr on 2021/8/18.
9 | */
10 | abstract class ITrackProvider {
11 |
12 | /**
13 | * Enable data statistics or not.
14 | */
15 | abstract var enabled: Boolean
16 |
17 | /**
18 | * The tag of this provider.
19 | */
20 | abstract var name: String
21 |
22 | /**
23 | * Init the provider.
24 | */
25 | @UiThread
26 | abstract fun onInit()
27 |
28 | /**
29 | * Do event track.
30 | */
31 | abstract fun onEvent(eventName: String, params: TrackParams)
32 | }
--------------------------------------------------------------------------------
/easytrack/src/main/java/com/pengxr/easytrack/core/TrackModel.kt:
--------------------------------------------------------------------------------
1 | package com.pengxr.easytrack.core
2 |
3 | import androidx.annotation.CallSuper
4 | import java.io.Serializable
5 |
6 | /**
7 | * 数据节点
8 | * Created by pengxr on 2021/8/18.
9 | */
10 | class TrackModel : ITrackModel, Serializable {
11 |
12 | protected val params by lazy {
13 | TrackParams()
14 | }
15 |
16 | /**
17 | * 设置参数,会覆盖已有参数
18 | */
19 | operator fun set(key: String, value: Any?) {
20 | params[key] = value
21 | }
22 |
23 | /**
24 | * 获取参数
25 | */
26 | operator fun get(key: String) = params[key]
27 |
28 | /**
29 | * 设置参数,不会覆盖已有参数
30 | */
31 | fun setIfNull(key: String, value: Any?) {
32 | params.setIfNull(key, value)
33 | }
34 |
35 | /**
36 | * 获取参数,为空返回默认值
37 | */
38 | fun get(key: String, default: String?) = params.get(key, default)
39 |
40 | /**
41 | * 数据填充
42 | */
43 | @CallSuper
44 | override fun fillTrackParams(params: TrackParams) {
45 | // 合并当前参数
46 | params.merge(this.params)
47 | }
48 | }
--------------------------------------------------------------------------------
/easytrack/src/main/java/com/pengxr/easytrack/core/TrackParams.kt:
--------------------------------------------------------------------------------
1 | package com.pengxr.easytrack.core
2 |
3 | import java.io.Serializable
4 |
5 | /**
6 | * Track Params attach on the node
7 | * Created by pengxr on 2021/8/18.
8 | */
9 | open class TrackParams : Iterable, Serializable {
10 |
11 | /**
12 | * Internal data.
13 | */
14 | private val data = HashMap()
15 |
16 | /**
17 | * Set anywhere.
18 | */
19 | operator fun set(key: String, value: Any?): TrackParams {
20 | data[key] = value?.toString()
21 | return this
22 | }
23 |
24 | /**
25 | * Get by key.
26 | */
27 | operator fun get(key: String): String? = data[key]
28 |
29 | /**
30 | * Set if null.
31 | */
32 | fun setIfNull(key: String, value: Any?): TrackParams {
33 | val oldValue = data[key]
34 | if (null == oldValue) {
35 | data[key] = value?.toString()
36 | }
37 | return this
38 | }
39 |
40 | /**
41 | * Get or default.
42 | */
43 | fun get(key: String, default: String?): String? = data[key] ?: default
44 |
45 | /**
46 | * Merge other object into current object.
47 | */
48 | fun merge(other: TrackParams?): TrackParams {
49 | if (null != other) {
50 | for ((key, value) in other) {
51 | setIfNull(key, value)
52 | }
53 | }
54 | return this
55 | }
56 |
57 | /**
58 | * Create an iterator.
59 | */
60 | override fun iterator() = data.iterator()
61 |
62 | override fun toString(): String {
63 | return StringBuilder().apply {
64 | append("[")
65 | for ((key, value) in data) {
66 | append(" $key = $value ,")
67 | }
68 | deleteCharAt(this.length - 1)
69 | append("]")
70 | }.toString()
71 | }
72 | }
--------------------------------------------------------------------------------
/easytrack/src/main/java/com/pengxr/easytrack/impl/BaseTrackActivity.kt:
--------------------------------------------------------------------------------
1 | package com.pengxr.easytrack.impl
2 |
3 | import android.os.Bundle
4 | import androidx.annotation.CallSuper
5 | import androidx.annotation.LayoutRes
6 | import androidx.appcompat.app.AppCompatActivity
7 | import com.pengxr.easytrack.core.IPageTrackNode
8 | import com.pengxr.easytrack.core.ITrackNode
9 | import com.pengxr.easytrack.core.EasyTrack
10 | import com.pengxr.easytrack.core.TrackParams
11 | import com.pengxr.easytrack.util.getReferrerParams
12 |
13 | /**
14 | * Base Activity with event track,you don't have to used it.
15 | *
16 | * Created by pengxr on 10/9/2021
17 | */
18 | abstract class BaseTrackActivity : AppCompatActivity, IPageTrackNode {
19 |
20 | constructor ()
21 |
22 | constructor (@LayoutRes contentLayoutId: Int) : super(contentLayoutId)
23 |
24 | // The snapshot of referrer page node.
25 | private var referrerSnapshot: ITrackNode? = null
26 |
27 | protected val trackParams by lazy {
28 | TrackParams()
29 | }
30 |
31 | // ---------------------------------------------------------------------------------------------
32 | // Activity
33 | // ---------------------------------------------------------------------------------------------
34 |
35 | override fun onCreate(savedInstanceState: Bundle?) {
36 | super.onCreate(savedInstanceState)
37 |
38 | // Snapshot for referrer page node.
39 | getReferrerSnapshot()?.let { referrerParams ->
40 | referrerSnapshot = object : ITrackNode {
41 | override val parent: ITrackNode? = null
42 |
43 | override fun fillTrackParams(params: TrackParams) {
44 | params.merge(referrerParams)
45 | }
46 | }
47 | }
48 | }
49 |
50 | // ---------------------------------------------------------------------------------------------
51 | // public
52 | // ---------------------------------------------------------------------------------------------
53 |
54 | /**
55 | * Get params from referrer page node.
56 | */
57 | fun getReferrerSnapshot(): TrackParams? = intent.getReferrerParams()?.let { referrerParams ->
58 | TrackParams().apply {
59 | // Fill referrer params.
60 | fillReferrerKeyMap(referrerKeyMap(), referrerParams, this)
61 | fillReferrerKeyMap(EasyTrack.referrerKeyMap, referrerParams, this)
62 | }
63 | }
64 |
65 | // ---------------------------------------------------------------------------------------------
66 | // IPageTrackNode
67 | // ---------------------------------------------------------------------------------------------
68 |
69 | override fun referrerKeyMap(): Map? = null
70 |
71 | override fun referrerSnapshot(): ITrackNode? = referrerSnapshot
72 |
73 | @CallSuper
74 | override fun fillTrackParams(params: TrackParams) {
75 | params.merge(trackParams)
76 | // You can expose api here, it makes subclasses more convenient to pass parameters.
77 | }
78 |
79 | // ---------------------------------------------------------------------------------------------
80 | // protected
81 | // ---------------------------------------------------------------------------------------------
82 |
83 | private fun fillReferrerKeyMap(
84 | map: Map?, referrerParams: TrackParams, params: TrackParams
85 | ) {
86 | if (map.isNullOrEmpty()) {
87 | return
88 | }
89 | for ((fromKey, toKey) in map) {
90 | val toValue = referrerParams[fromKey]
91 | if (null != toValue) {
92 | params.setIfNull(toKey, toValue)
93 | }
94 | }
95 | }
96 | }
--------------------------------------------------------------------------------
/easytrack/src/main/java/com/pengxr/easytrack/impl/BaseTrackFragment.kt:
--------------------------------------------------------------------------------
1 | package com.pengxr.easytrack.impl
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import androidx.annotation.CallSuper
6 | import androidx.annotation.LayoutRes
7 | import androidx.fragment.app.Fragment
8 | import com.pengxr.easytrack.core.ITrackModel
9 | import com.pengxr.easytrack.core.TrackParams
10 | import com.pengxr.easytrack.util.trackModel
11 |
12 | /**
13 | * Base Fragment with event track,You don't have to used it.
14 | *
15 | * Created by pengxr on 10/9/2021
16 | */
17 | abstract class BaseTrackFragment : Fragment, ITrackModel {
18 |
19 | constructor ()
20 |
21 | constructor (@LayoutRes contentLayoutId: Int) : super(contentLayoutId)
22 |
23 | // ---------------------------------------------------------------------------------------------
24 | // Fragment
25 | // ---------------------------------------------------------------------------------------------
26 |
27 | @CallSuper
28 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
29 | super.onViewCreated(view, savedInstanceState)
30 |
31 | view.trackModel = this
32 | }
33 |
34 | // ---------------------------------------------------------------------------------------------
35 | // IPageTrackNode
36 | // ---------------------------------------------------------------------------------------------
37 |
38 | override fun fillTrackParams(params: TrackParams) {
39 | }
40 | }
--------------------------------------------------------------------------------
/easytrack/src/main/java/com/pengxr/easytrack/session/TrackEvent.kt:
--------------------------------------------------------------------------------
1 | package com.pengxr.easytrack.session
2 |
3 | import com.pengxr.easytrack.core.ITrackModel
4 | import com.pengxr.easytrack.core.TrackParams
5 |
6 | /**
7 | * 埋点事件
8 | *
9 | * Created by pengxr on 21/8/2021
10 | */
11 | class TrackEvent internal constructor(
12 | val eventName: String, var params: TrackParams?
13 | ) {
14 |
15 | /**
16 | * 声明需要使用埋点会话中的参数
17 | */
18 | fun with(clazz: Class) {
19 |
20 | }
21 |
22 | /**
23 | * 声明需要使用埋点会话中的参数
24 | */
25 | fun with(vararg params: Array) {
26 |
27 | }
28 |
29 | /**
30 | * 执行上报
31 | */
32 | fun emit() {
33 | params = params ?: TrackParams()
34 |
35 | }
36 | }
--------------------------------------------------------------------------------
/easytrack/src/main/java/com/pengxr/easytrack/session/TrackSession.kt:
--------------------------------------------------------------------------------
1 | package com.pengxr.easytrack.session
2 |
3 | import com.pengxr.easytrack.core.TrackParams
4 |
5 | /**
6 | * 埋点会话
7 | *
8 | * Created by pengxr on 21/8/2021
9 | */
10 | class TrackSession internal constructor() : TrackParams() {
11 |
12 | var isEnabled = true
13 |
14 | // private val map = HashMap, ITrackModel?>()
15 | //
16 | // operator fun set(clazz: Class, model: ITrackModel?) {
17 | // map[clazz] = model
18 | // }
19 | //
20 | // operator fun get(clazz: Class): ITrackModel? = map[clazz]
21 | }
--------------------------------------------------------------------------------
/easytrack/src/main/java/com/pengxr/easytrack/util/EasyTrackUtils.kt:
--------------------------------------------------------------------------------
1 | package com.pengxr.easytrack.util
2 |
3 | import android.app.Activity
4 | import android.content.Intent
5 | import android.os.Handler
6 | import android.os.Looper
7 | import android.util.Log
8 | import android.view.View
9 | import android.view.ViewGroup
10 | import androidx.activity.ComponentActivity
11 | import androidx.annotation.IdRes
12 | import androidx.annotation.MainThread
13 | import androidx.core.app.ActivityCompat
14 | import androidx.core.view.ViewCompat
15 | import androidx.fragment.app.DialogFragment
16 | import androidx.fragment.app.Fragment
17 | import androidx.lifecycle.Lifecycle
18 | import androidx.lifecycle.LifecycleObserver
19 | import androidx.lifecycle.LifecycleOwner
20 | import androidx.lifecycle.OnLifecycleEvent
21 | import androidx.recyclerview.widget.RecyclerView
22 | import com.pengxr.easytrack.R
23 | import com.pengxr.easytrack.core.*
24 | import kotlin.properties.ReadOnlyProperty
25 | import kotlin.reflect.KProperty
26 |
27 | /**
28 | * Delegate Property of TrackNode.
29 | *
30 | * Created by pengxr on 26/8/2021
31 | */
32 |
33 | private const val EXTRA_REFERRER_SNAPSHOT = "referrer_node"
34 |
35 | // -------------------------------------------------------------------------------------------------
36 | // Java
37 | // -------------------------------------------------------------------------------------------------
38 |
39 | fun trackNode(view: View): TrackModel {
40 | return TrackModel().apply {
41 | view.trackModel = this
42 | }
43 | }
44 |
45 | fun trackNode(holder: RecyclerView.ViewHolder): TrackModel {
46 | return TrackModel().apply {
47 | holder.itemView.trackModel = this
48 | }
49 | }
50 |
51 | fun trackNode(fragment: Fragment): TrackModel {
52 | return TrackModel().apply {
53 | fragment.requireView().trackModel = this
54 | }
55 | }
56 |
57 | // -------------------------------------------------------------------------------------
58 | // Kotlin TrackNodeProperty
59 | // -------------------------------------------------------------------------------------
60 |
61 | fun F.track(): TrackNodeProperty = FragmentTrackNodeProperty()
62 |
63 | fun RecyclerView.ViewHolder.track(): TrackNodeProperty =
64 | LazyTrackNodeProperty() viewFactory@{
65 | return@viewFactory itemView
66 | }
67 |
68 | fun View.track(): TrackNodeProperty = LazyTrackNodeProperty() viewFactory@{
69 | return@viewFactory it
70 | }
71 |
72 | // -------------------------------------------------------------------------------------
73 | // TrackNodeProperty
74 | // -------------------------------------------------------------------------------------
75 |
76 | private const val TAG = "TrackNodeProperty"
77 |
78 | interface TrackNodeProperty : ReadOnlyProperty {
79 |
80 | /**
81 | * 视图节点
82 | */
83 | fun getViewNode(thisRef: R): View
84 |
85 | /**
86 | * 清除
87 | */
88 | @MainThread
89 | fun clear()
90 | }
91 |
92 | class LazyTrackNodeProperty(
93 | private val viewFactory: (R) -> View
94 | ) : TrackNodeProperty {
95 |
96 | private var trackNode: TrackModel? = null
97 |
98 | @Suppress("UNCHECKED_CAST")
99 | @MainThread
100 | override fun getValue(thisRef: R, property: KProperty<*>): TrackModel {
101 | // Already attached
102 | trackNode?.let { return it }
103 |
104 | return TrackModel().also {
105 | this.trackNode = it
106 | }
107 | }
108 |
109 | @MainThread
110 | override fun clear() {
111 | trackNode = null
112 | }
113 |
114 | override fun getViewNode(thisRef: R) = viewFactory(thisRef)
115 | }
116 |
117 | abstract class LifecycleTrackNodeProperty : TrackNodeProperty {
118 |
119 | private var trackNode: TrackModel? = null
120 |
121 | protected abstract fun getLifecycleOwner(thisRef: R): LifecycleOwner
122 |
123 | @MainThread
124 | override fun getValue(thisRef: R, property: KProperty<*>): TrackModel {
125 | // Already attached
126 | trackNode?.let { return it }
127 |
128 | val lifecycle = getLifecycleOwner(thisRef).lifecycle
129 | val trackNode = TrackModel()
130 | if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
131 | Log.w(
132 | TAG,
133 | "Access to trackNode after Lifecycle is destroyed or hasn't created yet. " + "The instance of trackNode will be not cached."
134 | )
135 | // We can access to TrackNode after Fragment.onDestroyView(), but don't save it to prevent memory leak
136 | } else {
137 | lifecycle.addObserver(ClearOnDestroyLifecycleObserver(this))
138 | // attach
139 | this.trackNode = trackNode
140 | getViewNode(thisRef).trackModel = trackNode
141 | }
142 | return trackNode
143 | }
144 |
145 | @MainThread
146 | override fun clear() {
147 | trackNode = null
148 | }
149 |
150 | private class ClearOnDestroyLifecycleObserver(
151 | private val property: LifecycleTrackNodeProperty<*>
152 | ) : LifecycleObserver {
153 |
154 | private companion object {
155 | private val mainHandler = Handler(Looper.getMainLooper())
156 | }
157 |
158 | @MainThread
159 | @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
160 | fun onDestroy(owner: LifecycleOwner) {
161 | mainHandler.post { property.clear() }
162 | }
163 | }
164 | }
165 |
166 | class FragmentTrackNodeProperty : LifecycleTrackNodeProperty() {
167 |
168 | override fun getLifecycleOwner(thisRef: F): LifecycleOwner {
169 | try {
170 | return thisRef.viewLifecycleOwner
171 | } catch (ignored: IllegalStateException) {
172 | error("Fragment doesn't have view associated with it or the view has been destroyed")
173 | }
174 | }
175 |
176 | override fun getViewNode(thisRef: F) = thisRef.requireView()
177 | }
178 |
179 | class DialogFragmentTrackNodeProperty(
180 | ) : LifecycleTrackNodeProperty() {
181 |
182 | override fun getLifecycleOwner(thisRef: F): LifecycleOwner {
183 | return if (thisRef.showsDialog) {
184 | thisRef
185 | } else {
186 | try {
187 | thisRef.viewLifecycleOwner
188 | } catch (ignored: IllegalStateException) {
189 | error(
190 | "Fragment doesn't have view associated with it or the view has been destroyed"
191 | )
192 | }
193 | }
194 | }
195 |
196 | override fun getViewNode(thisRef: F) = thisRef.requireView()
197 | }
198 |
199 | // -------------------------------------------------------------------------------------
200 | // Utils
201 | // -------------------------------------------------------------------------------------
202 |
203 | private fun View.requireViewByIdCompat(@IdRes id: Int): V {
204 | return ViewCompat.requireViewById(this, id)
205 | }
206 |
207 | private fun Activity.requireViewByIdCompat(@IdRes id: Int): V {
208 | return ActivityCompat.requireViewById(this, id)
209 | }
210 |
211 | /**
212 | * Utility to find root view for ViewBinding in Activity
213 | */
214 | private fun findRootView(activity: Activity?): View? {
215 | val contentView = activity?.findViewById(android.R.id.content)
216 | return when (contentView?.childCount) {
217 | 1 -> contentView.getChildAt(0)
218 | 0 -> null
219 | else -> null
220 | }
221 | }
222 |
223 | private fun DialogFragment.getRootView(viewBindingRootId: Int): View {
224 | val dialog = checkNotNull(dialog) {
225 | "DialogFragment doesn't have dialog. Use viewBinding delegate after onCreateDialog"
226 | }
227 | val window = checkNotNull(dialog.window) { "Fragment's Dialog has no window" }
228 | return with(window.decorView) {
229 | if (viewBindingRootId != 0) requireViewByIdCompat(
230 | viewBindingRootId
231 | ) else this
232 | }
233 | }
234 |
235 | /**
236 | * Params from referrer page note.
237 | */
238 | fun Intent.setReferrerSnapshot(node: ITrackModel?) {
239 | if (null != node) {
240 | setReferrerSnapshot(fillTrackParams(node))
241 | }
242 | }
243 |
244 | fun Intent.setReferrerSnapshot(node: View?) {
245 | if (null != node) {
246 | setReferrerSnapshot(fillTrackParams(node))
247 | }
248 | }
249 |
250 | fun Intent.setReferrerSnapshot(params: TrackParams?) {
251 | if (null != params) {
252 | putExtra(EXTRA_REFERRER_SNAPSHOT, params)
253 | }
254 | }
255 |
256 | fun Intent.getReferrerParams(): TrackParams? {
257 | return getSerializableExtra(EXTRA_REFERRER_SNAPSHOT) as TrackParams?
258 | }
259 |
260 | /**
261 | * Attach track model on the view.
262 | */
263 | var View.trackModel: ITrackModel?
264 | get() = this.getTag(R.id.tag_id_track_model) as? ITrackModel
265 | set(value) {
266 | this.setTag(R.id.tag_id_track_model, value)
267 | }
268 |
269 | /**
270 | * Do event track, it will collect event Params around the node tree.
271 | */
272 | @JvmOverloads
273 | fun Activity?.trackEvent(eventName: String, params: TrackParams? = null) =
274 | findRootView(this)?.doTrackEvent(eventName, params)
275 |
276 | @JvmOverloads
277 | fun Fragment?.trackEvent(eventName: String, params: TrackParams? = null) =
278 | this?.requireView()?.doTrackEvent(eventName, params)
279 |
280 | @JvmOverloads
281 | fun RecyclerView.ViewHolder?.trackEvent(eventName: String, params: TrackParams? = null) {
282 | this?.itemView?.let {
283 | if (null == it.parent) {
284 | it.post { it.doTrackEvent(eventName, params) }
285 | } else {
286 | it.doTrackEvent(eventName, params)
287 | }
288 | }
289 | }
290 |
291 | @JvmOverloads
292 | fun View?.trackEvent(eventName: String, params: TrackParams? = null): TrackParams? =
293 | this?.doTrackEvent(eventName, params)
--------------------------------------------------------------------------------
/easytrack/src/main/java/com/pengxr/easytrack/util/LogUtil.kt:
--------------------------------------------------------------------------------
1 | package com.pengxr.easytrack.util
2 |
3 | /**
4 | *
5 | *
6 | *
7 | * Created by pengxr on 6/9/2021
8 | */
--------------------------------------------------------------------------------
/easytrack/src/main/java/com/pengxr/easytrack/util/ViewUtils.kt:
--------------------------------------------------------------------------------
1 | package com.pengxr.easytrack.util
2 |
3 | import android.app.Activity
4 | import android.content.ContextWrapper
5 | import android.view.View
6 |
7 | /**
8 | * Created by pengxr on 10/9/2021
9 | */
10 |
11 | /**
12 | * try get host activity from view.
13 | * views hosted on floating window like dialog and toast will sure return null.
14 | *
15 | * @return host activity; or null if not available
16 | */
17 | internal fun getActivityFromView(view: View): Activity? {
18 | var context = view.context
19 | while (context is ContextWrapper) {
20 | if (context is Activity) {
21 | return context
22 | }
23 | context = context.baseContext
24 | }
25 | return null
26 | }
27 |
--------------------------------------------------------------------------------
/easytrack/src/main/res/values/ids.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat Aug 14 14:45:01 CST 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/images/EasyTrack - 困难.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/images/EasyTrack - 困难.png
--------------------------------------------------------------------------------
/images/EasyTrack - 流程.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/images/EasyTrack - 流程.png
--------------------------------------------------------------------------------
/images/EasyTrack - 目录.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/images/EasyTrack - 目录.png
--------------------------------------------------------------------------------
/images/EasyTrack - 西瓜1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/images/EasyTrack - 西瓜1.png
--------------------------------------------------------------------------------
/images/EasyTrack - 西瓜2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/images/EasyTrack - 西瓜2.png
--------------------------------------------------------------------------------
/sample/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/sample/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply from: '../config.gradle'
3 |
4 | project.ext.configAppModule project
5 |
6 | dependencies {
7 | implementation SDK.ktx_jdk7
8 | implementation SDK.core_ktx
9 | implementation SDK.ktx_coroutines
10 | implementation SDK.appcompat
11 | implementation SDK.activityx
12 | implementation SDK.activityx_ktx
13 | implementation SDK.fragmentx
14 | implementation SDK.fragmentx_ktx
15 | implementation SDK.recyclerview
16 | implementation SDK.material
17 | implementation SDK.constraintlayout
18 | implementation SDK.coordinatorlayout
19 | implementation SDK.lifecycle_extensions
20 |
21 | implementation project(':easytrack')
22 | implementation 'com.github.pengxurui:KotlinUtil:1.0.1'
23 | }
--------------------------------------------------------------------------------
/sample/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/sample/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/pengxr/sample/base/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package com.pengxr.sample.base
2 |
3 | import androidx.annotation.CallSuper
4 | import com.pengxr.easytrack.core.TrackParams
5 | import com.pengxr.easytrack.impl.BaseTrackActivity
6 | import com.pengxr.sample.statistics.EventConstants.CUR_PAGE
7 |
8 | /**
9 | * Created by pengxr on 11/9/2021
10 | */
11 | abstract class BaseActivity : BaseTrackActivity() {
12 |
13 | // ---------------------------------------------------------------------------------------------
14 | // BaseTrackActivity
15 | // ---------------------------------------------------------------------------------------------
16 |
17 | @CallSuper
18 | override fun fillTrackParams(params: TrackParams) {
19 | super.fillTrackParams(params)
20 | getCurPage()?.also {
21 | params.setIfNull(CUR_PAGE, it)
22 | }
23 | }
24 |
25 | // ---------------------------------------------------------------------------------------------
26 | // protected
27 | // ---------------------------------------------------------------------------------------------
28 |
29 | protected open fun getCurPage(): String? = null
30 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/pengxr/sample/base/MyApplication.kt:
--------------------------------------------------------------------------------
1 | package com.pengxr.sample.base
2 |
3 | import android.app.Application
4 |
5 | /**
6 | * Created by pengxr on 5/9/2021
7 | */
8 | class MyApplication : Application() {
9 |
10 | override fun onCreate() {
11 | super.onCreate()
12 |
13 | // Init statistics lib.
14 | init(applicationContext)
15 | }
16 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/pengxr/sample/entity/GoodsItem.kt:
--------------------------------------------------------------------------------
1 | package com.pengxr.sample.entity
2 |
3 | import android.os.Parcelable
4 | import androidx.annotation.DrawableRes
5 | import kotlinx.parcelize.Parcelize
6 |
7 | /**
8 | * Created by pengxr on 6/9/2021
9 | */
10 | @Parcelize
11 | class GoodsItem(
12 | var id: String,
13 | var goods_name: String,
14 | var goods_content: String,
15 | @DrawableRes
16 | var goods_icon: Int
17 | ) : Parcelable {
18 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/pengxr/sample/entity/StoreDetail.kt:
--------------------------------------------------------------------------------
1 | package com.pengxr.sample.entity
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 |
6 |
7 | /**
8 | * Created by pengxr on 5/9/2021
9 | */
10 | @Parcelize
11 | class StoreDetail(
12 | var id: String,
13 | var store_name: String,
14 | ) : Parcelable {
15 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/pengxr/sample/goods/view/GoodsDetailActivity.java:
--------------------------------------------------------------------------------
1 | package com.pengxr.sample.goods.view;
2 |
3 | import android.content.Context;
4 | import android.content.Intent;
5 | import android.os.Bundle;
6 | import android.view.View;
7 |
8 | import com.pengxr.easytrack.core.TrackParams;
9 | import com.pengxr.easytrack.util.EasyTrackUtilsKt;
10 | import com.pengxr.sample.base.BaseActivity;
11 | import com.pengxr.sample.databinding.GoodsDetailActivityBinding;
12 | import com.pengxr.sample.entity.GoodsItem;
13 | import com.pengxr.sample.goods.vm.GoodsDetailViewModel;
14 | import com.pengxr.sample.utils.ToastUtil;
15 | import com.pengxr.sample.utils.VMCompat;
16 |
17 | import java.util.HashMap;
18 | import java.util.Map;
19 |
20 | import androidx.annotation.Nullable;
21 |
22 | import static com.pengxr.sample.statistics.EventConstants.GOODS_DETAIL_NAME;
23 | import static com.pengxr.sample.statistics.EventConstants.GOODS_ID;
24 | import static com.pengxr.sample.statistics.EventConstants.GOODS_NAME;
25 | import static com.pengxr.sample.statistics.EventConstants.SHARE_CLICK_STEP1;
26 | import static com.pengxr.sample.statistics.EventConstants.STORE_ID;
27 | import static com.pengxr.sample.statistics.EventConstants.STORE_NAME;
28 |
29 | /**
30 | *
31 | * Created by pengxr on 6/9/2021
32 | */
33 |
34 | public class GoodsDetailActivity extends BaseActivity {
35 |
36 | private static final String EXTRA_GOODS = "extra_goods";
37 |
38 | private GoodsDetailViewModel mViewModel;
39 | private GoodsDetailActivityBinding binding;
40 |
41 | public static void start(Context context, GoodsItem item, TrackParams params) {
42 | Intent intent = new Intent(context, GoodsDetailActivity.class);
43 | intent.putExtra(EXTRA_GOODS, item);
44 | EasyTrackUtilsKt.setReferrerSnapshot(intent, params);
45 | context.startActivity(intent);
46 | }
47 |
48 | @Nullable
49 | @Override
50 | protected String getCurPage() {
51 | return GOODS_DETAIL_NAME;
52 | }
53 |
54 | @Nullable
55 | @Override
56 | public Map referrerKeyMap() {
57 | Map map = new HashMap<>();
58 | map.put(STORE_ID, STORE_ID);
59 | map.put(STORE_NAME, STORE_NAME);
60 | return map;
61 | }
62 |
63 | @Override
64 | protected void onCreate(@Nullable Bundle savedInstanceState) {
65 | super.onCreate(savedInstanceState);
66 |
67 | GoodsItem item = getIntent().getParcelableExtra(EXTRA_GOODS);
68 | mViewModel = VMCompat.get(this, GoodsDetailViewModel.class);
69 | mViewModel.init(item);
70 |
71 | binding = GoodsDetailActivityBinding.inflate(getLayoutInflater());
72 | setContentView(binding.getRoot());
73 |
74 | init();
75 | }
76 |
77 | private void init() {
78 | initTrack();
79 | initView();
80 | }
81 |
82 | private void initTrack() {
83 | // Add track params.
84 | getTrackParams().set(GOODS_ID, mViewModel.getGoodsItem().getId());
85 | getTrackParams().set(GOODS_NAME, mViewModel.getGoodsItem().getGoods_name());
86 | }
87 |
88 | private void initView() {
89 | binding.titleGoodsHome.tvTitle.setText("商品详情");
90 | binding.titleGoodsHome.ivBack.setVisibility(View.VISIBLE);
91 | binding.titleGoodsHome.ivBack.setOnClickListener(new View.OnClickListener() {
92 | @Override
93 | public void onClick(View view) {
94 | finish();
95 | }
96 | });
97 | binding.titleGoodsHome.ivShare.setOnClickListener(new View.OnClickListener() {
98 | @Override
99 | public void onClick(View view) {
100 | ToastUtil.toast(GoodsDetailActivity.this, "分享商品");
101 | EasyTrackUtilsKt.trackEvent(binding.titleGoodsHome.ivShare, SHARE_CLICK_STEP1);
102 | }
103 | });
104 | }
105 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/pengxr/sample/goods/view/GoodsDetailFragment.java:
--------------------------------------------------------------------------------
1 | package com.pengxr.sample.goods.view;
2 |
3 | import android.os.Bundle;
4 | import android.view.LayoutInflater;
5 | import android.view.View;
6 | import android.view.ViewGroup;
7 |
8 | import com.pengxr.easytrack.core.ITrackModel;
9 | import com.pengxr.easytrack.core.TrackParams;
10 | import com.pengxr.easytrack.util.EasyTrackUtilsKt;
11 | import com.pengxr.sample.R;
12 | import com.pengxr.sample.databinding.GoodsDetailFragmentBinding;
13 | import com.pengxr.sample.entity.GoodsItem;
14 | import com.pengxr.sample.goods.vm.GoodsDetailViewModel;
15 | import com.pengxr.sample.statistics.EventConstants;
16 | import com.pengxr.sample.utils.VMCompat;
17 |
18 | import androidx.annotation.NonNull;
19 | import androidx.annotation.Nullable;
20 | import androidx.fragment.app.Fragment;
21 |
22 | import static com.pengxr.sample.statistics.EventConstants.GOODS_DETAIL_CLICK;
23 | import static com.pengxr.sample.statistics.EventConstants.GOODS_DETAIL_IMAGE_NAME;
24 |
25 | /**
26 | * Created by pengxr on 10/9/2021
27 | */
28 | public class GoodsDetailFragment extends Fragment implements ITrackModel {
29 |
30 | private GoodsDetailViewModel mViewModel;
31 | private GoodsDetailFragmentBinding binding;
32 |
33 | public GoodsDetailFragment() {
34 | super(R.layout.goods_detail_fragment);
35 | }
36 |
37 | @Override
38 | public void fillTrackParams(@NonNull TrackParams params) {
39 | params.set(EventConstants.CUR_PAGE, GOODS_DETAIL_IMAGE_NAME);
40 | }
41 |
42 | @Nullable
43 | @Override
44 | public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
45 | binding = GoodsDetailFragmentBinding.inflate(inflater);
46 | return binding.getRoot();
47 | }
48 |
49 | @Override
50 | public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
51 | super.onViewCreated(view, savedInstanceState);
52 |
53 | EasyTrackUtilsKt.setTrackModel(view, this);
54 |
55 | init();
56 | }
57 |
58 | private void init() {
59 | mViewModel = VMCompat.get(getActivity(), GoodsDetailViewModel.class);
60 |
61 | initView();
62 | }
63 |
64 | private void initView() {
65 | GoodsItem item = mViewModel.getGoodsItem();
66 | binding.ivImage.setImageResource(item.getGoods_icon());
67 | binding.tvTitle.setText(item.getGoods_name());
68 | binding.tvContent.setText(item.getGoods_content());
69 |
70 | binding.ivImage.setOnClickListener(new View.OnClickListener() {
71 | @Override
72 | public void onClick(View view) {
73 | EasyTrackUtilsKt.trackEvent(view, GOODS_DETAIL_CLICK);
74 | }
75 | });
76 | }
77 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/pengxr/sample/goods/vm/GoodsDetailViewModel.java:
--------------------------------------------------------------------------------
1 | package com.pengxr.sample.goods.vm;
2 |
3 | import com.pengxr.sample.entity.GoodsItem;
4 |
5 | import androidx.lifecycle.ViewModel;
6 |
7 | /**
8 | * Created by pengxr on 2021/9/15.
9 | */
10 | public class GoodsDetailViewModel extends ViewModel {
11 |
12 | private GoodsItem mGoodsItem;
13 |
14 | public void init(GoodsItem item) {
15 | this.mGoodsItem = item;
16 | }
17 |
18 | public GoodsItem getGoodsItem() {
19 | return mGoodsItem;
20 | }
21 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/pengxr/sample/statistics/EventConstants.java:
--------------------------------------------------------------------------------
1 | package com.pengxr.sample.statistics;
2 |
3 | /**
4 | * Created by pengxr on 5/9/2021
5 | */
6 | public class EventConstants {
7 |
8 | // ---------------------------------------------------------------------------------------------
9 | // Event Name
10 | // ---------------------------------------------------------------------------------------------
11 |
12 | // Click on a goods item.
13 | public static final String GOODS_CLICK = "goods_click";
14 | // Expose a goods item.
15 | public static final String GOODS_EXPOSE = "goods_expose";
16 | // Click on a share button.
17 | public static final String SHARE_CLICK_STEP1 = "share_click_step_1";
18 | // Click on a Share button, and choose a share platform.
19 | public static final String SHARE_CLICK_STEP2 = "share_click_step_2";
20 | // Click on a goods detail.
21 | public static final String GOODS_DETAIL_CLICK = "goods_detail_click";
22 |
23 | // ---------------------------------------------------------------------------------------------
24 | // Params Key
25 | // ---------------------------------------------------------------------------------------------
26 |
27 | public static final String FROM_PAGE = "from_page";
28 | public static final String CUR_PAGE = "cur_page";
29 | public static final String FROM_TAB = "from_tab";
30 | public static final String CUR_TAB = "cur_tab";
31 | public static final String DEVICE_ID = "device_id";
32 | public static final String LONGITUDE = "longitude";
33 | public static final String LATITUDE = "latitude";
34 | public static final String CITY_ID = "city_id";
35 | public static final String CITY_NAME = "city_name";
36 |
37 | public static final String STORE_ID = "store_id";
38 | public static final String STORE_NAME = "store_name";
39 | public static final String GOODS_ID = "goods_id";
40 | public static final String GOODS_NAME = "goods_name";
41 |
42 | // ---------------------------------------------------------------------------------------------
43 | // Page Name.
44 | // ---------------------------------------------------------------------------------------------
45 |
46 | public static final String STORE_HOME_NAME = "store_home";
47 | public static final String GOODS_DETAIL_NAME = "goods_detail";
48 | public static final String GOODS_DETAIL_IMAGE_NAME = "goods_detail_image";
49 | }
50 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/pengxr/sample/statistics/SensorProvider.kt:
--------------------------------------------------------------------------------
1 | package com.pengxr.sample.statistics
2 |
3 | import android.util.Log
4 | import com.pengxr.easytrack.core.ITrackProvider
5 | import com.pengxr.easytrack.core.TrackParams
6 | import java.util.*
7 | import kotlin.collections.HashMap
8 |
9 | /**
10 | * Mock Sensor SDK.
11 | *
12 | * Created by pengxr on 5/9/2021
13 | */
14 | class SensorProvider : ITrackProvider() {
15 |
16 | companion object {
17 | const val TAG = "Sensor"
18 | }
19 |
20 | // Mock internal datas.
21 | private val data = HashMap()
22 |
23 | /**
24 | * Enable data statistics or not.
25 | */
26 | override var enabled = true
27 |
28 | /**
29 | * The tag of this provider.
30 | */
31 | override var name = TAG
32 |
33 | /**
34 | * Init the provider.
35 | */
36 | override fun onInit() {
37 | Log.d(TAG, "Init Sensor provider.")
38 |
39 | registerSuperProperties()
40 | }
41 |
42 | /**
43 | * Do event track.
44 | */
45 | override fun onEvent(eventName: String, params: TrackParams) {
46 | Log.d(TAG, params.toString())
47 | }
48 |
49 | private fun registerSuperProperties() {
50 | val params = TrackParams()
51 | params[EventConstants.LONGITUDE] = "113.9"
52 | params[EventConstants.LATITUDE] = 22.5
53 | params[EventConstants.CITY_ID] = "1"
54 | params[EventConstants.CITY_NAME] = "深圳市"
55 | params[EventConstants.DEVICE_ID] = UUID.randomUUID()
56 | for ((key, value) in params) {
57 | data[key] = value
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/pengxr/sample/statistics/StatisticsUtils.kt:
--------------------------------------------------------------------------------
1 | package com.pengxr.sample.base
2 |
3 | import android.content.Context
4 | import com.pengxr.easytrack.core.EasyTrack
5 | import com.pengxr.sample.BuildConfig
6 | import com.pengxr.sample.statistics.EventConstants.*
7 | import com.pengxr.sample.statistics.SensorProvider
8 | import com.pengxr.sample.statistics.UmengProvider
9 |
10 | /**
11 | * Created by pengxr on 5/9/2021
12 | */
13 |
14 | val umengProvider by lazy {
15 | UmengProvider()
16 | }
17 |
18 | val sensorProvider by lazy {
19 | SensorProvider()
20 | }
21 |
22 | /**
23 | * @param context ApplicationContext
24 | */
25 | fun init(context: Context) {
26 | configStatistics(context)
27 | registerProviders(context)
28 | }
29 |
30 | private fun configStatistics(context: Context) {
31 | EasyTrack.debug = BuildConfig.DEBUG
32 | EasyTrack.referrerKeyMap = mapOf(
33 | CUR_PAGE to FROM_PAGE,
34 | CUR_TAB to FROM_TAB
35 | )
36 | }
37 |
38 | private fun registerProviders(context: Context) {
39 | EasyTrack.registerProvider(umengProvider)
40 | EasyTrack.registerProvider(sensorProvider)
41 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/pengxr/sample/statistics/UmengProvider.kt:
--------------------------------------------------------------------------------
1 | package com.pengxr.sample.statistics
2 |
3 | import android.util.Log
4 | import com.pengxr.easytrack.core.ITrackProvider
5 | import com.pengxr.easytrack.core.TrackParams
6 | import java.util.*
7 | import kotlin.collections.HashMap
8 |
9 | /**
10 | * Mock Umeng SDK.
11 | *
12 | * Created by pengxr on 5/9/2021
13 | */
14 | class UmengProvider : ITrackProvider() {
15 |
16 | companion object {
17 | const val TAG = "Umeng"
18 | }
19 |
20 | // Mock internal datas.
21 | private val data = HashMap()
22 |
23 | /**
24 | * Enable data statistics or not.
25 | */
26 | override var enabled = true
27 |
28 | /**
29 | * The tag of this provider.
30 | */
31 | override var name = TAG
32 |
33 | /**
34 | * Init the provider.
35 | */
36 | override fun onInit() {
37 | Log.d(TAG, "Init Umeng provider.")
38 |
39 | registerSuperProperties()
40 | }
41 |
42 | /**
43 | * Do event track.
44 | */
45 | override fun onEvent(eventName: String, params: TrackParams) {
46 | Log.d(TAG, params.toString())
47 | }
48 |
49 | private fun registerSuperProperties() {
50 | val params = TrackParams()
51 | params[EventConstants.LONGITUDE] = "113.9"
52 | params[EventConstants.LATITUDE] = 22.5
53 | params[EventConstants.CITY_ID] = "1"
54 | params[EventConstants.CITY_NAME] = "深圳市"
55 | params[EventConstants.DEVICE_ID] = UUID.randomUUID()
56 | for ((key, value) in params) {
57 | data[key] = value
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/pengxr/sample/store/view/StoreHomeActivity.kt:
--------------------------------------------------------------------------------
1 | package com.pengxr.sample.store.view
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import androidx.fragment.app.FragmentPagerAdapter
6 | import com.pengxr.easytrack.util.trackEvent
7 | import com.pengxr.ktx.delegate.viewBinding
8 | import com.pengxr.sample.R
9 | import com.pengxr.sample.base.BaseActivity
10 | import com.pengxr.sample.databinding.StoreHomeActivityBinding
11 | import com.pengxr.sample.statistics.EventConstants.*
12 | import com.pengxr.sample.store.vm.StoreHomeViewModel
13 | import com.pengxr.sample.utils.ToastUtil
14 | import com.pengxr.sample.utils.VMCompat
15 |
16 | /**
17 | * Created by pengxr on 5/9/2021
18 | */
19 | class StoreHomeActivity : BaseActivity() {
20 |
21 | private val viewModel by lazy {
22 | VMCompat.get(this, StoreHomeViewModel::class.java)
23 | }
24 |
25 | private val binding by viewBinding(StoreHomeActivityBinding::bind)
26 |
27 | override fun getCurPage() = STORE_HOME_NAME
28 |
29 | override fun onCreate(savedInstanceState: Bundle?) {
30 | super.onCreate(savedInstanceState)
31 | setContentView(R.layout.store_home_activity)
32 |
33 | init()
34 | }
35 |
36 | private fun init() {
37 | initView()
38 | initObserve()
39 |
40 | fetchData()
41 | }
42 |
43 | private fun initView() {
44 | with(binding) {
45 | titleStoreHome.ivBack.visibility = View.INVISIBLE
46 | titleStoreHome.ivShare.setOnClickListener { ivShare ->
47 | ToastUtil.toast(this@StoreHomeActivity, "分享商店")
48 | ivShare.trackEvent(SHARE_CLICK_STEP1)
49 | }
50 | val titles = arrayOf("推荐", "最新")
51 | val fragments = arrayOf(StoreRecommendFragment(), StoreNewestFragment())
52 |
53 | for (title in titles) {
54 | tabStoreHome.addTab(tabStoreHome.newTab().setText(title))
55 | }
56 | pagerStoreHome.adapter = object : FragmentPagerAdapter(supportFragmentManager) {
57 | override fun getCount() = fragments.size
58 |
59 | override fun getItem(position: Int) = fragments[position]
60 |
61 | override fun getPageTitle(position: Int) = titles[position]
62 | }
63 | tabStoreHome.setupWithViewPager(pagerStoreHome, false)
64 | }
65 | }
66 |
67 | private fun initObserve() {
68 | viewModel.storeDetailLiveData.observe(this) { detail ->
69 | // Add track params.
70 | trackParams[STORE_ID] = detail.id
71 | trackParams[STORE_NAME] = detail.store_name
72 | binding.titleStoreHome.tvTitle.text = detail.store_name
73 | }
74 | }
75 |
76 | private fun fetchData() {
77 | viewModel.fetchData(this)
78 | }
79 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/pengxr/sample/store/view/StoreNewestFragment.kt:
--------------------------------------------------------------------------------
1 | package com.pengxr.sample.store.view
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import androidx.fragment.app.Fragment
7 | import androidx.recyclerview.widget.RecyclerView
8 | import androidx.recyclerview.widget.StaggeredGridLayoutManager
9 | import com.pengxr.easytrack.util.track
10 | import com.pengxr.ktx.delegate.viewBinding
11 | import com.pengxr.sample.R
12 | import com.pengxr.sample.databinding.LayoutFragmentBinding
13 | import com.pengxr.sample.entity.GoodsItem
14 | import com.pengxr.sample.statistics.EventConstants.CUR_PAGE
15 | import com.pengxr.sample.store.vm.StoreHomeViewModel
16 | import com.pengxr.sample.store.widget.GoodsViewHolder
17 | import com.pengxr.sample.store.widget.inflater
18 | import com.pengxr.sample.utils.VMCompat
19 | import com.pengxr.sample.widget.SpacingDecoration
20 |
21 | /**
22 | * Created by pengxr on 11/9/2021
23 | */
24 | class StoreNewestFragment : Fragment(R.layout.layout_fragment) {
25 |
26 | private val viewModel by lazy {
27 | VMCompat.get(requireActivity(), StoreHomeViewModel::class.java)
28 | }
29 |
30 | private val binding by viewBinding(LayoutFragmentBinding::bind)
31 |
32 | private val trackNode by track()
33 |
34 | private val adapter = GoodsAdapter()
35 |
36 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
37 | super.onViewCreated(view, savedInstanceState)
38 |
39 | init()
40 | }
41 |
42 | private fun init() {
43 | initView()
44 | initTrack()
45 | }
46 |
47 | private fun initView() {
48 | with(binding) {
49 | rv.layoutManager = StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
50 | rv.addItemDecoration(SpacingDecoration(requireContext(), 8, 8).apply {
51 | setOutSpacing(requireContext(), 8, 8, 8, 8)
52 | })
53 | rv.adapter = adapter
54 | }
55 |
56 | viewModel.newestGoodsList.observe(viewLifecycleOwner) { list ->
57 | adapter.setData(list)
58 | }
59 | viewModel.fetchNewestGoodsList(requireContext())
60 | }
61 |
62 | private fun initTrack() {
63 | trackNode[CUR_PAGE] = "Newest"
64 | }
65 |
66 | private class GoodsAdapter : RecyclerView.Adapter() {
67 |
68 | private val data = ArrayList()
69 |
70 | fun setData(data: List?) {
71 | this.data.clear()
72 | data?.let {
73 | this.data.addAll(data)
74 | }
75 | notifyDataSetChanged()
76 | }
77 |
78 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
79 | inflater(parent.context, parent)
80 |
81 | override fun onBindViewHolder(holder: GoodsViewHolder, position: Int) =
82 | holder.bind(data[position])
83 |
84 | override fun getItemCount() = data.size
85 | }
86 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/pengxr/sample/store/view/StoreRecommendFragment.kt:
--------------------------------------------------------------------------------
1 | package com.pengxr.sample.store.view
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import androidx.fragment.app.Fragment
7 | import androidx.recyclerview.widget.RecyclerView
8 | import androidx.recyclerview.widget.StaggeredGridLayoutManager
9 | import androidx.recyclerview.widget.StaggeredGridLayoutManager.VERTICAL
10 | import com.pengxr.easytrack.util.track
11 | import com.pengxr.ktx.delegate.viewBinding
12 | import com.pengxr.sample.R
13 | import com.pengxr.sample.databinding.LayoutFragmentBinding
14 | import com.pengxr.sample.entity.GoodsItem
15 | import com.pengxr.sample.statistics.EventConstants.CUR_PAGE
16 | import com.pengxr.sample.store.vm.StoreHomeViewModel
17 | import com.pengxr.sample.store.widget.GoodsViewHolder
18 | import com.pengxr.sample.store.widget.inflater
19 | import com.pengxr.sample.utils.VMCompat
20 | import com.pengxr.sample.widget.SpacingDecoration
21 |
22 | /**
23 | * Created by pengxr on 5/9/2021
24 | */
25 | class StoreRecommendFragment : Fragment(R.layout.layout_fragment) {
26 |
27 | private val viewModel by lazy {
28 | VMCompat.get(requireActivity(), StoreHomeViewModel::class.java)
29 | }
30 |
31 | private val binding by viewBinding(LayoutFragmentBinding::bind)
32 |
33 | private val trackNode by track()
34 |
35 | private val adapter = GoodsAdapter()
36 |
37 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
38 | super.onViewCreated(view, savedInstanceState)
39 |
40 | init()
41 | }
42 |
43 | private fun init() {
44 | initView()
45 | initTrack()
46 | }
47 |
48 | private fun initView() {
49 | with(binding) {
50 | rv.layoutManager = StaggeredGridLayoutManager(2, VERTICAL)
51 | rv.addItemDecoration(SpacingDecoration(requireContext(), 8, 8).apply {
52 | setOutSpacing(requireContext(), 8, 8, 8, 8)
53 | })
54 | rv.adapter = adapter
55 | }
56 |
57 | viewModel.recommendGoodsList.observe(viewLifecycleOwner) { list ->
58 | adapter.setData(list)
59 | }
60 | viewModel.fetchRecommendGoodsList(requireContext())
61 | }
62 |
63 | private fun initTrack() {
64 | trackNode[CUR_PAGE] = "Recommend"
65 | }
66 |
67 | private class GoodsAdapter : RecyclerView.Adapter() {
68 |
69 | private val data = ArrayList()
70 |
71 | fun setData(data: List?) {
72 | this.data.clear()
73 | data?.let {
74 | this.data.addAll(data)
75 | }
76 | notifyDataSetChanged()
77 | }
78 |
79 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
80 | inflater(parent.context, parent)
81 |
82 | override fun onBindViewHolder(holder: GoodsViewHolder, position: Int) =
83 | holder.bind(data[position])
84 |
85 | override fun getItemCount() = data.size
86 | }
87 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/pengxr/sample/store/vm/StoreHomeViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.pengxr.sample.store.vm
2 |
3 | import android.content.Context
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.ViewModel
6 | import com.pengxr.sample.R
7 | import com.pengxr.sample.entity.GoodsItem
8 | import com.pengxr.sample.entity.StoreDetail
9 |
10 | /**
11 | * Created by pengxr on 5/9/2021
12 | */
13 | class StoreHomeViewModel : ViewModel() {
14 |
15 | val storeDetailLiveData = MutableLiveData()
16 |
17 | val recommendGoodsList = MutableLiveData>()
18 |
19 | val newestGoodsList = MutableLiveData>()
20 |
21 | val storeDetail: StoreDetail?
22 | get() = storeDetailLiveData.value
23 |
24 | fun fetchData(context: Context) {
25 | storeDetailLiveData.value = StoreDetail("10000", "商店")
26 | }
27 |
28 | fun fetchRecommendGoodsList(context: Context) {
29 | recommendGoodsList.value = listOf(
30 | GoodsItem("1000", "自行车", "没有轮子的自行车", R.drawable.icon_bike),
31 | GoodsItem("1001", "冰淇淋", "已融化的冰淇淋", R.drawable.icon_icecream),
32 | GoodsItem("1002", "吉他", "断了弦的吉他", R.drawable.icon_guita),
33 | GoodsItem("1003", "包子", "吃剩下的包子", R.drawable.icon_baozi),
34 | GoodsItem("1004", "猫", "会咬人的猫", R.drawable.icon_cat),
35 | GoodsItem("1005", "面条", "隔夜的面条", R.drawable.icon_noodle)
36 | )
37 | }
38 |
39 | fun fetchNewestGoodsList(context: Context) {
40 | newestGoodsList.value = listOf(
41 | GoodsItem("1000", "自行车", "没有轮子的自行车", R.drawable.icon_bike),
42 | GoodsItem("1001", "冰淇淋", "已融化的冰淇淋", R.drawable.icon_icecream),
43 | GoodsItem("1002", "吉他", "断了弦的吉他", R.drawable.icon_guita),
44 | GoodsItem("1003", "包子", "吃剩下的包子", R.drawable.icon_baozi),
45 | GoodsItem("1004", "猫", "会咬人的猫", R.drawable.icon_cat),
46 | GoodsItem("1005", "面条", "隔夜的面条", R.drawable.icon_noodle)
47 | )
48 | }
49 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/pengxr/sample/store/widget/GoodsViewHolder.kt:
--------------------------------------------------------------------------------
1 | package com.pengxr.sample.store.widget
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.graphics.Outline
6 | import android.os.Build
7 | import android.view.LayoutInflater
8 | import android.view.View
9 | import android.view.ViewGroup
10 | import android.view.ViewOutlineProvider
11 | import androidx.recyclerview.widget.RecyclerView
12 | import com.pengxr.easytrack.util.track
13 | import com.pengxr.easytrack.util.trackEvent
14 | import com.pengxr.ktx.delegate.viewBinding
15 | import com.pengxr.sample.R
16 | import com.pengxr.sample.databinding.StoreHomeGoodsItemBinding
17 | import com.pengxr.sample.entity.GoodsItem
18 | import com.pengxr.sample.goods.view.GoodsDetailActivity
19 | import com.pengxr.sample.statistics.EventConstants.*
20 | import com.pengxr.sample.utils.DensityUtil
21 |
22 | /**
23 | * Created by pengxr on 6/9/2021
24 | */
25 |
26 | fun inflater(context: Context, parent: ViewGroup) = GoodsViewHolder(
27 | LayoutInflater.from(context).inflate(R.layout.store_home_goods_item, parent, false)
28 | )
29 |
30 | class GoodsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
31 |
32 | private var mItem: GoodsItem? = null
33 | private val binding by viewBinding(StoreHomeGoodsItemBinding::bind)
34 |
35 | private val trackNode by track()
36 |
37 | init {
38 | itemView.setOnClickListener {
39 | mItem?.let { item ->
40 | itemView.trackEvent(GOODS_CLICK)?.also {
41 | GoodsDetailActivity.start(itemView.context, item, it)
42 | }
43 | }
44 | }
45 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
46 | itemView.outlineProvider = object : ViewOutlineProvider() {
47 | @SuppressLint("NewApi")
48 | override fun getOutline(view: View, outline: Outline) {
49 | val d: Int = DensityUtil.dip2px(view.context, 6F)
50 | outline.setRoundRect(0, 0, view.measuredWidth, view.measuredHeight, d.toFloat())
51 | }
52 | }
53 | itemView.clipToOutline = true
54 | }
55 | }
56 |
57 | fun bind(item: GoodsItem) {
58 | mItem = item
59 | with(binding) {
60 | tvTitle.text = item.goods_name
61 | tvContent.text = item.goods_content
62 | ivImage.setImageResource(item.goods_icon)
63 |
64 | trackNode[GOODS_ID] = item.id
65 | trackNode[GOODS_NAME] = item.goods_name
66 |
67 | trackEvent(GOODS_EXPOSE)
68 | }
69 | }
70 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/pengxr/sample/utils/DensityUtil.java:
--------------------------------------------------------------------------------
1 | package com.pengxr.sample.utils;
2 |
3 | import android.content.Context;
4 |
5 | public class DensityUtil {
6 |
7 | public static int dip2px(Context context, float dpValue) {
8 | float scale = context.getResources().getDisplayMetrics().density;
9 | return (int) (dpValue * scale + 0.5f);
10 | }
11 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/pengxr/sample/utils/ToastUtil.java:
--------------------------------------------------------------------------------
1 | package com.pengxr.sample.utils;
2 |
3 | import android.content.Context;
4 | import android.widget.Toast;
5 |
6 | /**
7 | * Created by pengxr on 5/9/2021
8 | */
9 | public class ToastUtil {
10 |
11 | public static void toast(Context context, String str) {
12 | Toast.makeText(context, str, Toast.LENGTH_SHORT).show();
13 | }
14 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/pengxr/sample/utils/VMCompat.java:
--------------------------------------------------------------------------------
1 | package com.pengxr.sample.utils;
2 |
3 | import androidx.annotation.NonNull;
4 | import androidx.fragment.app.Fragment;
5 | import androidx.fragment.app.FragmentActivity;
6 | import androidx.lifecycle.ViewModel;
7 | import androidx.lifecycle.ViewModelProvider;
8 | import androidx.lifecycle.ViewModelProviders;
9 | import androidx.lifecycle.ViewModelStoreOwner;
10 |
11 | public class VMCompat {
12 |
13 | public static T get(@NonNull ViewModelStoreOwner owner,
14 | @NonNull Class modelClass) {
15 | return new ViewModelProvider(owner).get(modelClass);
16 | }
17 | }
--------------------------------------------------------------------------------
/sample/src/main/java/com/pengxr/sample/widget/CircleImageView.java:
--------------------------------------------------------------------------------
1 | package com.pengxr.sample.widget;
2 |
3 | import android.content.Context;
4 | import android.content.res.TypedArray;
5 | import android.graphics.Bitmap;
6 | import android.graphics.BitmapShader;
7 | import android.graphics.Canvas;
8 | import android.graphics.Color;
9 | import android.graphics.Matrix;
10 | import android.graphics.Paint;
11 | import android.graphics.Rect;
12 | import android.graphics.RectF;
13 | import android.graphics.Shader;
14 | import android.graphics.drawable.BitmapDrawable;
15 | import android.graphics.drawable.ColorDrawable;
16 | import android.graphics.drawable.Drawable;
17 | import android.net.Uri;
18 | import android.util.AttributeSet;
19 |
20 | import androidx.appcompat.widget.AppCompatImageView;
21 |
22 | public class CircleImageView extends AppCompatImageView {
23 |
24 | // private static final ScaleType SCALE_TYPE = ScaleType.CENTER_CROP;
25 |
26 | private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888;
27 | private static final int COLORDRAWABLE_DIMENSION = 1;
28 |
29 | private static final int DEFAULT_BORDER_WIDTH = 0;
30 | private static final int DEFAULT_BORDER_COLOR = Color.BLACK;
31 |
32 | private final RectF mDrawableRect = new RectF();
33 | private final RectF mBorderRect = new RectF();
34 |
35 | private final Matrix mShaderMatrix = new Matrix();
36 | private final Paint mBitmapPaint = new Paint();
37 | private final Paint mBorderPaint = new Paint();
38 |
39 | private int mBorderColor = DEFAULT_BORDER_COLOR;
40 | private int mBorderWidth = DEFAULT_BORDER_WIDTH;
41 | private int mCornerWidth = DEFAULT_BORDER_WIDTH;
42 |
43 | private Bitmap mBitmap;
44 | private BitmapShader mBitmapShader;
45 | private int mBitmapWidth;
46 | private int mBitmapHeight;
47 |
48 | private float mDrawableRadius;
49 | private float mBorderRadius;
50 |
51 | private boolean mReady;
52 | private boolean mSetupPending;
53 |
54 | public CircleImageView(Context context) {
55 | super(context);
56 |
57 | init();
58 | }
59 |
60 | public CircleImageView(Context context, AttributeSet attrs) {
61 | this(context, attrs, 0);
62 | }
63 |
64 | public CircleImageView(Context context, AttributeSet attrs, int defStyle) {
65 | super(context, attrs, defStyle);
66 |
67 | TypedArray a = context.obtainStyledAttributes(attrs, com.pengxr.sample.R.styleable.CircleImageView, defStyle, 0);
68 |
69 | mBorderWidth = a.getDimensionPixelSize(com.pengxr.sample.R.styleable.CircleImageView_border_width, DEFAULT_BORDER_WIDTH);
70 | mBorderColor = a.getColor(com.pengxr.sample.R.styleable.CircleImageView_border_color, DEFAULT_BORDER_COLOR);
71 | mCornerWidth = a.getDimensionPixelSize(com.pengxr.sample.R.styleable.CircleImageView_corner_width, DEFAULT_BORDER_WIDTH);
72 |
73 | a.recycle();
74 |
75 | init();
76 | }
77 |
78 | private void init() {
79 | // super.setScaleType(SCALE_TYPE);
80 | mReady = true;
81 |
82 | if (mSetupPending) {
83 | setup();
84 | mSetupPending = false;
85 | }
86 | }
87 |
88 | // @Override
89 | // public ScaleType getScaleType() {
90 | // return SCALE_TYPE;
91 | // }
92 | //
93 | // @Override
94 | // public void setScaleType(ScaleType scaleType) {
95 | // if (scaleType != SCALE_TYPE) {
96 | // throw new IllegalArgumentException(String.format("ScaleType %s not supported.", scaleType));
97 | // }
98 | // }
99 |
100 | @Override
101 | protected void onDraw(Canvas canvas) {
102 | if (getDrawable() == null) {
103 | return;
104 | }
105 | if (mCornerWidth == 0) {
106 | canvas.drawCircle(getWidth() / 2, getHeight() / 2, mDrawableRadius, mBitmapPaint);
107 | if (mBorderWidth != 0) {
108 | canvas.drawCircle(getWidth() / 2, getHeight() / 2, mBorderRadius, mBorderPaint);
109 | }
110 | } else {
111 | canvas.drawRoundRect(new RectF(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(), getHeight() - getPaddingBottom()),
112 | mCornerWidth, mCornerWidth, mBitmapPaint);
113 | }
114 | }
115 |
116 | @Override
117 | protected void onSizeChanged(int w, int h, int oldw, int oldh) {
118 | super.onSizeChanged(w, h, oldw, oldh);
119 | setup();
120 | }
121 |
122 | public int getBorderColor() {
123 | return mBorderColor;
124 | }
125 |
126 | public void setBorderColor(int borderColor) {
127 | if (borderColor == mBorderColor) {
128 | return;
129 | }
130 |
131 | mBorderColor = borderColor;
132 | mBorderPaint.setColor(mBorderColor);
133 | invalidate();
134 | }
135 |
136 | public int getBorderWidth() {
137 | return mBorderWidth;
138 | }
139 |
140 | public void setBorderWidth(int borderWidth) {
141 | if (borderWidth == mBorderWidth) {
142 | return;
143 | }
144 |
145 | mBorderWidth = borderWidth;
146 | setup();
147 | }
148 |
149 | @Override
150 | public void setImageBitmap(Bitmap bm) {
151 | super.setImageBitmap(bm);
152 | mBitmap = bm;
153 | setup();
154 | }
155 |
156 | @Override
157 | public void setImageDrawable(Drawable drawable) {
158 | super.setImageDrawable(drawable);
159 | mBitmap = getBitmapFromDrawable(drawable);
160 | setup();
161 | }
162 |
163 | @Override
164 | public void setImageResource(int resId) {
165 | super.setImageResource(resId);
166 | mBitmap = getBitmapFromDrawable(getDrawable());
167 | setup();
168 | }
169 |
170 | @Override
171 | public void setImageURI(Uri uri) {
172 | super.setImageURI(uri);
173 | mBitmap = getBitmapFromDrawable(getDrawable());
174 | setup();
175 | }
176 |
177 | private Bitmap getBitmapFromDrawable(Drawable drawable) {
178 | if (drawable == null) {
179 | return null;
180 | }
181 |
182 | if (drawable instanceof BitmapDrawable) {
183 | return ((BitmapDrawable) drawable).getBitmap();
184 | }
185 |
186 | try {
187 | Bitmap bitmap;
188 |
189 | if (drawable instanceof ColorDrawable) {
190 | bitmap = Bitmap.createBitmap(COLORDRAWABLE_DIMENSION, COLORDRAWABLE_DIMENSION, BITMAP_CONFIG);
191 | } else {
192 | int intrinsicWidth = drawable.getIntrinsicWidth();
193 | int intrinsicHeight = drawable.getIntrinsicHeight();
194 |
195 | if (intrinsicWidth <= 0 || intrinsicHeight <= 0) {
196 | Rect bounds = drawable.getBounds();
197 | intrinsicWidth = bounds.width();
198 | intrinsicHeight = bounds.height();
199 | }
200 |
201 | if (intrinsicWidth <= 0 || intrinsicHeight <= 0) {
202 | intrinsicWidth = intrinsicHeight = COLORDRAWABLE_DIMENSION;
203 | }
204 |
205 | bitmap = Bitmap.createBitmap(intrinsicWidth, intrinsicHeight, BITMAP_CONFIG);
206 | }
207 |
208 | Canvas canvas = new Canvas(bitmap);
209 | drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
210 | drawable.draw(canvas);
211 | return bitmap;
212 | } catch (OutOfMemoryError e) {
213 | return null;
214 | }
215 | }
216 |
217 | private void setup() {
218 | if (!mReady) {
219 | mSetupPending = true;
220 | return;
221 | }
222 |
223 | if (mBitmap == null) {
224 | return;
225 | }
226 |
227 | mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
228 |
229 | mBitmapPaint.setAntiAlias(true);
230 | mBitmapPaint.setShader(mBitmapShader);
231 |
232 | mBorderPaint.setStyle(Paint.Style.STROKE);
233 | mBorderPaint.setAntiAlias(true);
234 | mBorderPaint.setColor(mBorderColor);
235 | mBorderPaint.setStrokeWidth(mBorderWidth);
236 |
237 | mBitmapHeight = mBitmap.getHeight();
238 | mBitmapWidth = mBitmap.getWidth();
239 |
240 | mBorderRect.set(0, 0, getWidth(), getHeight());
241 | mBorderRadius = Math.min((mBorderRect.height() - mBorderWidth) / 2, (mBorderRect.width() - mBorderWidth) / 2);
242 |
243 | mDrawableRect.set(mBorderWidth, mBorderWidth, mBorderRect.width() - mBorderWidth, mBorderRect.height() - mBorderWidth);
244 | mDrawableRadius = Math.min(mDrawableRect.height() / 2, mDrawableRect.width() / 2);
245 |
246 | updateShaderMatrix();
247 | invalidate();
248 | }
249 |
250 | private void updateShaderMatrix() {
251 | float scale;
252 | float dx = 0;
253 | float dy = 0;
254 |
255 | mShaderMatrix.set(null);
256 |
257 | if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) {
258 | scale = mDrawableRect.height() / (float) mBitmapHeight;
259 | dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f;
260 | } else {
261 | scale = mDrawableRect.width() / (float) mBitmapWidth;
262 | dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f;
263 | }
264 |
265 | mShaderMatrix.setScale(scale, scale);
266 | mShaderMatrix.postTranslate((int) (dx + 0.5f) + mBorderWidth, (int) (dy + 0.5f) + mBorderWidth);
267 |
268 | mBitmapShader.setLocalMatrix(mShaderMatrix);
269 | }
270 |
271 | }
272 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/pengxr/sample/widget/HackyViewPager.java:
--------------------------------------------------------------------------------
1 | package com.pengxr.sample.widget;
2 |
3 | import android.content.Context;
4 | import android.util.AttributeSet;
5 | import android.view.MotionEvent;
6 |
7 | import androidx.viewpager.widget.ViewPager;
8 |
9 | /**
10 | * Hacky fix for Issue #4 and
11 | * http://code.google.com/p/android/issues/detail?id=18990
12 | *
13 | * ScaleGestureDetector seems to mess up the touch events, which means that
14 | * ViewGroups which make use of onInterceptTouchEvent throw a lot of
15 | * IllegalArgumentException: pointerIndex out of range.
16 | *
17 | * There's not much I can do in my code for now, but we can mask the result by
18 | * just catching the problem and ignoring it.
19 | *
20 | * @author Chris Banes
21 | */
22 | public class HackyViewPager extends ViewPager {
23 |
24 | public HackyViewPager(Context context) {
25 | super(context);
26 | }
27 |
28 | public HackyViewPager(Context context, AttributeSet attrs) {
29 | super(context, attrs);
30 | }
31 |
32 | @Override
33 | public boolean onInterceptTouchEvent(MotionEvent ev) {
34 | try {
35 | return super.onInterceptTouchEvent(ev);
36 | } catch (IllegalArgumentException e) {
37 | e.printStackTrace();
38 | return false;
39 | }
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/pengxr/sample/widget/HeightAutoFitSquareImageView.java:
--------------------------------------------------------------------------------
1 | package com.pengxr.sample.widget;
2 |
3 | import android.content.Context;
4 | import android.util.AttributeSet;
5 |
6 | import androidx.appcompat.widget.AppCompatImageView;
7 |
8 | /**
9 | * Square picture with Height adaptive.
10 | */
11 | public class HeightAutoFitSquareImageView extends AppCompatImageView {
12 |
13 | public HeightAutoFitSquareImageView(Context context) {
14 | super(context);
15 | }
16 |
17 | public HeightAutoFitSquareImageView(Context context, AttributeSet attrs) {
18 | super(context, attrs);
19 | }
20 |
21 | public HeightAutoFitSquareImageView(Context context, AttributeSet attrs, int defStyleAttr) {
22 | super(context, attrs, defStyleAttr);
23 | }
24 |
25 | @Override
26 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
27 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
28 | int measuredW = getMeasuredWidth();
29 | setMeasuredDimension(measuredW, measuredW);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/pengxr/sample/widget/ImmersiveStatusBarSpace.java:
--------------------------------------------------------------------------------
1 | package com.pengxr.sample.widget;
2 |
3 | import android.content.Context;
4 | import android.os.Build;
5 | import android.util.AttributeSet;
6 | import android.view.View;
7 |
8 | import java.lang.reflect.Field;
9 |
10 | public class ImmersiveStatusBarSpace extends View {
11 |
12 | private int height;
13 |
14 | public ImmersiveStatusBarSpace(Context context) {
15 | this(context, (AttributeSet) null);
16 | }
17 |
18 | public ImmersiveStatusBarSpace(Context context, AttributeSet attrs) {
19 | this(context, attrs, 0);
20 | }
21 |
22 | public ImmersiveStatusBarSpace(Context context, AttributeSet attrs, int defStyleAttr) {
23 | super(context, attrs, defStyleAttr);
24 | this.height = Build.VERSION.SDK_INT >= 19 ? getStatusBarHeight(this.getContext()) : 0;
25 | }
26 |
27 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
28 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
29 | this.setMeasuredDimension(this.getMeasuredWidth(), this.height);
30 | }
31 |
32 | public static int getStatusBarHeight(Context context) {
33 | Class> c = null;
34 | Object obj = null;
35 | Field field = null;
36 | int x = 0, sbar = 0;
37 | try {
38 | c = Class.forName("com.android.internal.R$dimen");
39 | obj = c.newInstance();
40 | field = c.getField("status_bar_height");
41 | x = Integer.parseInt(field.get(obj).toString());
42 | sbar = context.getResources().getDimensionPixelSize(x);
43 | } catch (Exception E) {
44 | E.printStackTrace();
45 | }
46 | return sbar;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/sample/src/main/java/com/pengxr/sample/widget/SpacingDecoration.java:
--------------------------------------------------------------------------------
1 | package com.pengxr.sample.widget;
2 |
3 | import android.content.Context;
4 | import android.graphics.Color;
5 | import android.graphics.Paint;
6 | import android.graphics.Rect;
7 | import android.view.View;
8 |
9 | import com.pengxr.sample.utils.DensityUtil;
10 |
11 | import androidx.annotation.ColorInt;
12 | import androidx.annotation.Dimension;
13 | import androidx.annotation.NonNull;
14 | import androidx.recyclerview.widget.GridLayoutManager;
15 | import androidx.recyclerview.widget.LinearLayoutManager;
16 | import androidx.recyclerview.widget.RecyclerView;
17 | import androidx.recyclerview.widget.StaggeredGridLayoutManager;
18 |
19 | import static androidx.annotation.Dimension.DP;
20 |
21 | /**
22 | * Created by pengxr on 2019/12/17.
23 | */
24 | public class SpacingDecoration extends RecyclerView.ItemDecoration {
25 |
26 | @NonNull
27 | private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
28 | // 内边距
29 | @Dimension
30 | private int mRowSpacing = 0;
31 | @Dimension
32 | private int mColumnSpacing = 0;
33 | // 四个外边距
34 | @Dimension
35 | private int mOutLeftSpacing = 0;
36 | @Dimension
37 | private int mOutTopSpacing = 0;
38 | @Dimension
39 | private int mOutRightSpacing = 0;
40 | @Dimension
41 | private int mOutBottomSpacing = 0;
42 |
43 | public SpacingDecoration() {
44 | }
45 |
46 | public SpacingDecoration(Context context, @Dimension(unit = DP) int inSpacingDp) {
47 | this(context, inSpacingDp, inSpacingDp, Color.TRANSPARENT);
48 | }
49 |
50 | public SpacingDecoration(Context context, @Dimension(unit = DP) int rowSpacingDp, @Dimension(unit = DP) int columnSpacingDp) {
51 | this(context, rowSpacingDp, columnSpacingDp, Color.TRANSPARENT);
52 | }
53 |
54 | public SpacingDecoration(Context context,
55 | @Dimension(unit = DP) int rowSpacingDp,
56 | @Dimension(unit = DP) int columnSpacingDp, @ColorInt int decorationColor) {
57 | mRowSpacing = DensityUtil.dip2px(context, rowSpacingDp);
58 | mColumnSpacing = DensityUtil.dip2px(context, columnSpacingDp);
59 |
60 | mPaint.setColor(decorationColor);
61 | }
62 |
63 | /**
64 | * 设置外边距
65 | */
66 | public void setOutSpacing(Context context,
67 | @Dimension(unit = DP) int leftDp,
68 | @Dimension(unit = DP) int topDp,
69 | @Dimension(unit = DP) int rightDp,
70 | @Dimension(unit = DP) int bottomDp) {
71 | mOutLeftSpacing = DensityUtil.dip2px(context, leftDp);
72 | mOutTopSpacing = DensityUtil.dip2px(context, topDp);
73 | mOutRightSpacing = DensityUtil.dip2px(context, rightDp);
74 | mOutBottomSpacing = DensityUtil.dip2px(context, bottomDp);
75 | }
76 |
77 | public void setDecorationColor(@ColorInt int decorationColor) {
78 | mPaint.setColor(decorationColor);
79 | }
80 |
81 | // ---------------------------------------------------------------------------------------------
82 |
83 | @Override
84 | public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State _state) {
85 | // super 中将 outRect 四边置 0
86 | super.getItemOffsets(outRect, view, parent, _state);
87 |
88 | // 位置
89 | int position = parent.getChildAdapterPosition(view);
90 | // 总数
91 | int itemCount = parent.getAdapter().getItemCount();
92 | if (position >= itemCount) {
93 | return;
94 | }
95 |
96 | if (parent.getLayoutManager() instanceof GridLayoutManager) {
97 | // 网格
98 | getItemOffsetForGrid(outRect, (GridLayoutManager) parent.getLayoutManager(), position, itemCount);
99 | } else if (parent.getLayoutManager() instanceof StaggeredGridLayoutManager) {
100 | // 瀑布
101 | getItemOffsetForStagger(outRect, view, (StaggeredGridLayoutManager) parent.getLayoutManager(), position, itemCount);
102 | } else if (parent.getLayoutManager() instanceof LinearLayoutManager) {
103 | // 线性
104 | getItemOffsetsForLinear(outRect, (LinearLayoutManager) parent.getLayoutManager(), position, itemCount);
105 | }
106 | }
107 |
108 | /**
109 | * 处理线性布局
110 | */
111 | private void getItemOffsetsForLinear(@NonNull Rect outRect, LinearLayoutManager lm, int position, int count) {
112 | if (lm.getOrientation() == LinearLayoutManager.HORIZONTAL) {
113 | // 横向
114 | outRect.top = mOutTopSpacing;
115 | outRect.bottom = mOutBottomSpacing;
116 | // 第一个
117 | if (0 == position) {
118 | outRect.left = mOutLeftSpacing;
119 | }
120 | // 内边距
121 | if (position > 0) {
122 | outRect.left = mColumnSpacing;
123 | }
124 | // 最后一个(只有一项item时,即是第一个也是最后一个)
125 | if (position == count - 1) {
126 | outRect.right = mOutRightSpacing;
127 | }
128 | } else {
129 | // 纵向
130 | outRect.left = mOutLeftSpacing;
131 | outRect.right = mOutRightSpacing;
132 | // 第一个
133 | if (0 == position) {
134 | outRect.top = mOutTopSpacing;
135 | }
136 | // 内边距
137 | if (position > 0) {
138 | outRect.top = mRowSpacing;
139 | }
140 | // 最后一个(只有一项item时,即是第一个也是最后一个)
141 | if (position == count - 1) {
142 | outRect.bottom = mOutBottomSpacing;
143 | }
144 | }
145 | }
146 |
147 | /**
148 | * 处理网格布局
149 | */
150 | private void getItemOffsetForGrid(@NonNull Rect outRect, @NonNull GridLayoutManager lm, int position, int count) {
151 | int spanCount = lm.getSpanCount();
152 | int column = position % spanCount;
153 | getGridItemOffsets(outRect, position, count, column, spanCount, lm.getOrientation());
154 | }
155 |
156 | /**
157 | * 处理瀑布流布局
158 | */
159 | private void getItemOffsetForStagger(@NonNull Rect outRect, @NonNull View view, @NonNull StaggeredGridLayoutManager lm, int position, int count) {
160 | int spanCount = lm.getSpanCount();
161 | StaggeredGridLayoutManager.LayoutParams lp = (StaggeredGridLayoutManager.LayoutParams) view.getLayoutParams();
162 | int column = lp.getSpanIndex();
163 | getGridItemOffsets(outRect, position, count, column, spanCount, lm.getOrientation());
164 | }
165 |
166 | private void getGridItemOffsets(Rect outRect, int position, int count, int column, int spanCount, int orientation) {
167 | if (0 == orientation) {
168 | // 横向
169 | if (position < spanCount) {
170 | outRect.left = mOutLeftSpacing;
171 | }
172 | if (count - position < spanCount) {
173 | outRect.right = mOutRightSpacing;
174 | }
175 | if (position >= spanCount) {
176 | outRect.left = mColumnSpacing;
177 | }
178 | if (0 == column) {
179 | outRect.top = mOutTopSpacing;
180 | }
181 | if (column >= 0 && column < spanCount - 1) {
182 | outRect.bottom = mRowSpacing / 2; // 误差?
183 | }
184 | if (column > 0 && column <= spanCount - 1) {
185 | outRect.top = mRowSpacing / 2;
186 | }
187 | if (column == spanCount - 1) {
188 | outRect.bottom = mOutBottomSpacing;
189 | }
190 | } else {
191 | // 纵向
192 | if (position < spanCount) {
193 | outRect.top = mOutTopSpacing;
194 | }
195 | if (position >= spanCount) {
196 | outRect.top = mRowSpacing;
197 | }
198 | if (count - position < spanCount) {
199 | outRect.bottom = mOutBottomSpacing;
200 | }
201 | if (0 == column) {
202 | outRect.left = mOutLeftSpacing;
203 | }
204 | if (column >= 0 && column < spanCount - 1) {
205 | outRect.right = mColumnSpacing / 2; // 误差?
206 | }
207 | if (column > 0 && column <= spanCount - 1) {
208 | outRect.left = mColumnSpacing / 2;
209 | }
210 | if (column == spanCount - 1) {
211 | outRect.right = mOutRightSpacing;
212 | }
213 | }
214 | }
215 |
216 | // @Override
217 | // public void onDraw(@NonNull Canvas canvas, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
218 | // int left = parent.getPaddingLeft();
219 | // int right = parent.getWidth() - parent.getPaddingRight();
220 | //
221 | // int childCount = parent.getChildCount();
222 | // for (int index = 0; index < childCount; index++) {
223 | // View child = parent.getChildAt(index);
224 | // RecyclerView.LayoutParams layoutParams = (RecyclerView.LayoutParams) child.getLayoutParams();
225 | // int childPosition = layoutParams.getViewAdapterPosition();
226 | // // 绘制分割线
227 | // left += layoutParams.leftMargin;
228 | // right -= layoutParams.rightMargin;
229 | // int top = child.getTop() - layoutParams.topMargin - mInjector.getGroupTitleHeight(childPosition);
230 | // int bottom = child.getTop() - layoutParams.topMargin;
231 | // canvas.drawRect(left, top, right, bottom, mPaint);
232 | // }
233 | // }
234 | }
235 |
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-xhdpi/ic_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/sample/src/main/res/drawable-xhdpi/ic_back.png
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-xhdpi/ic_share.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/sample/src/main/res/drawable-xhdpi/ic_share.png
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-xxhdpi/ic_back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/sample/src/main/res/drawable-xxhdpi/ic_back.png
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-xxhdpi/ic_launcher.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/sample/src/main/res/drawable-xxhdpi/ic_launcher.jpg
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-xxhdpi/ic_share.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/sample/src/main/res/drawable-xxhdpi/ic_share.png
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-xxhdpi/icon_baozi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/sample/src/main/res/drawable-xxhdpi/icon_baozi.png
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-xxhdpi/icon_bike.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/sample/src/main/res/drawable-xxhdpi/icon_bike.png
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-xxhdpi/icon_cat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/sample/src/main/res/drawable-xxhdpi/icon_cat.png
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-xxhdpi/icon_guita.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/sample/src/main/res/drawable-xxhdpi/icon_guita.png
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-xxhdpi/icon_icecream.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/sample/src/main/res/drawable-xxhdpi/icon_icecream.png
--------------------------------------------------------------------------------
/sample/src/main/res/drawable-xxhdpi/icon_noodle.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pengxurui/EasyTrack/13935b2a162b3c14f428ccb7a8c4c9b4e2358fc6/sample/src/main/res/drawable-xxhdpi/icon_noodle.jpg
--------------------------------------------------------------------------------
/sample/src/main/res/layout/goods_detail_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 |
12 |
19 |
20 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/goods_detail_fragment.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
15 |
16 |
30 |
31 |
45 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/layout_fragment.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/layout_title.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
16 |
17 |
26 |
27 |
39 |
40 |
49 |
50 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/store_home_activity.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
12 |
13 |
14 |
24 |
25 |
26 |
27 |
34 |
35 |
--------------------------------------------------------------------------------
/sample/src/main/res/layout/store_home_goods_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
17 |
18 |
25 |
26 |
42 |
43 |
59 |
60 |
--------------------------------------------------------------------------------
/sample/src/main/res/values-night/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | EasyTrack
3 |
--------------------------------------------------------------------------------
/sample/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = "EasyTrack"
2 | include ':sample'
3 | include ':easytrack'
4 |
--------------------------------------------------------------------------------