add TriangleView, Fix RecyclerViewDataManager, remove UpgradeChecker

androidx
Ztiany 5 years ago
parent 35d7360f08
commit 2ab637234c
  1. 2
      lib_base/src/main/java/com/android/base/adapter/recycler/RecyclerDataManagerImpl.java
  2. 137
      lib_base/src/main/java/com/android/base/utils/upgrade/ApkDownloader.java
  3. 228
      lib_base/src/main/java/com/android/base/utils/upgrade/AppUpgradeChecker.kt
  4. 16
      lib_base/src/main/java/com/android/base/utils/upgrade/UpgradeInfo.kt
  5. 35
      lib_base/src/main/java/com/android/base/utils/upgrade/UpgradeInteractor.kt
  6. 110
      lib_base/src/main/java/com/android/base/utils/upgrade/UpgradeService.java
  7. 18
      lib_base/src/main/java/com/android/base/widget/shape/Direction.kt
  8. 208
      lib_base/src/main/java/com/android/base/widget/shape/TriangleView.kt
  9. 24
      lib_base/src/main/res/values/base_attrs.xml
  10. 26
      lib_coroutines/lib_coroutines.iml

@ -110,7 +110,7 @@ final class RecyclerDataManagerImpl<T> implements DataManager<T> {
@Override
public void replace(T oldElem, T newElem) {
if (mData != null && mData.contains(newElem)) {
if (mData != null && mData.contains(oldElem)) {
replaceAt(mData.indexOf(oldElem), newElem);
}
}

@ -1,137 +0,0 @@
package com.android.base.utils.upgrade;
import com.android.base.utils.common.Files;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import static com.blankj.utilcode.util.CloseUtils.closeIOQuietly;
/**
* 下载APK
*
* @author Ztiany
* Email: 1169654504@qq.com
* Date : 2017-01-04 10:40
*/
final class ApkDownloader {
private String mUrl;
private String mDigitalAbstract;
private ApkDownloaderListener mApkDownloaderListener;
private File mDesFile;
private File mTempDesFile;
private static final String TEMP_MASK = "temp_%s";
private long mNotifyTime = 0;
ApkDownloader(String url, String desFilePath, String digitalAbstract, ApkDownloaderListener apkDownloaderListener) {
mUrl = url;
mDigitalAbstract = digitalAbstract;
mApkDownloaderListener = apkDownloaderListener;
mDesFile = new File(desFilePath);
String name = mDesFile.getName();
mTempDesFile = new File(mDesFile.getParent(), String.format(TEMP_MASK, name));
}
void start() {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.interceptors().clear();
OkHttpClient okHttpClient = builder.build();
Request request = new Request.Builder()
.url(mUrl)
.build();
ResponseBody body;
try {
Response response = okHttpClient.newCall(request).execute();
if (response.isSuccessful()) {
body = response.body();
} else {
mApkDownloaderListener.onFail(new RuntimeException("网络错误"));
return;
}
} catch (IOException e) {
e.printStackTrace();
mApkDownloaderListener.onFail(e);
return;
}
//检查body
if (body == null) {
mApkDownloaderListener.onFail(new RuntimeException("网络错误"));
return;
}
if (mDesFile.exists() && mDesFile.length() == body.contentLength() && AppUpgradeChecker.upgradeInteractor.checkApkFile(mDesFile, mDigitalAbstract)) {
mApkDownloaderListener.onSuccess(mDesFile);
} else {
Files.makeParentPath(mDesFile);
Files.makeParentPath(mTempDesFile);
boolean success = startDownload(body);
if (success) {
boolean renameToSuccess = mTempDesFile.renameTo(mDesFile);
if (renameToSuccess) {
if (AppUpgradeChecker.upgradeInteractor.checkApkFile(mDesFile, mDigitalAbstract)) {
mApkDownloaderListener.onSuccess(mDesFile);
} else {
mApkDownloaderListener.onFail(new RuntimeException("数字摘要对比失败"));
}
}
}
}
}
private boolean startDownload(ResponseBody body) {
InputStream is = null;
FileOutputStream fos = null;
try {
byte[] buf = new byte[1024 * 2];//缓冲器
int len;//每次读取的长度
assert body != null;
final long total = body.contentLength();//总的长度
is = body.byteStream();
long sum = 0;
fos = new FileOutputStream(mTempDesFile);
while ((len = is.read(buf)) != -1) {
sum += len;
fos.write(buf, 0, len);
if ((System.currentTimeMillis() - mNotifyTime) >= 300) {
mNotifyTime = System.currentTimeMillis();
mApkDownloaderListener.onProgress(total, sum);
}
}
mApkDownloaderListener.onProgress(total, total);
fos.flush();
return true;
} catch (IOException e) {
e.printStackTrace();
mApkDownloaderListener.onFail(e);
return false;
} finally {
closeIOQuietly(is, fos);
}
}
interface ApkDownloaderListener {
void onProgress(long total, long progress);
void onSuccess(File desFile);
void onFail(Exception e);
}
}

@ -1,228 +0,0 @@
package com.android.base.utils.upgrade
import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.android.base.app.Sword
import com.android.base.data.State
import com.android.base.rx.observeOnUI
import com.android.base.utils.BaseUtils
import com.android.base.utils.android.XAppUtils
import com.app.base.upgrade.UpgradeInteractor
import com.blankj.utilcode.util.AppUtils
import com.blankj.utilcode.util.Utils
import timber.log.Timber
import java.io.File
/**
* A tool for checking app upgrade.
*
* usage, when automatic check new version:
*
* ```
* AppUpgradeChecker.checkAppUpgrade()
* ```
*
* usage, when click to check new version:
* ```
* if (AppUpgradeChecker.isDownloading) {
* showMessage("正在下载更新")
* return@setOnClickListener
* }
*
* AppUpgradeChecker.checkAppUpgrade(false)
*
* if (!subscribed) {
* subscribeUpgradeInfo()
* subscribed = true
* }
* ```
*/
object AppUpgradeChecker {
private var successOnce = false
var isDownloading = false
private set
val isChecking: Boolean
get() = currentState.isLoading
private var currentState = State.noChange<UpgradeInfo>()
set(value) {
field = value
notifyStateChanged(value)
}
private fun notifyStateChanged(state: State<UpgradeInfo>) {
innerLiveState.value = state
}
private val innerLiveState = MutableLiveData<State<UpgradeInfo>>()
val upgradeState: LiveData<State<UpgradeInfo>>
get() = innerLiveState
private var resultReceiver: ResultBroadcastReceiver? = null
private val isUpgradeServiceRunning: Boolean
get() = XAppUtils.isServiceRunning(BaseUtils.getAppContext(), UpgradeService::class.java.name)
private lateinit var upgradeInfo: UpgradeInfo
lateinit var upgradeInteractor: UpgradeInteractor
fun installInteractor(upgradeInteractor: UpgradeInteractor) {
AppUpgradeChecker.upgradeInteractor = upgradeInteractor
}
fun checkAppUpgrade(silence: Boolean = true) {
Timber.d("checkAppUpgrade-->currentState == $currentState")
/*正在检查*/
if (currentState.isLoading) {
return
}
/*已经检查过了*/
if (silence && successOnce) {
return
}
/*正在下载*/
if (isUpgradeServiceRunning) {
return
}
realCheck()
}
private fun realCheck() {
currentState = State.loading()
upgradeInteractor.checkUpgrade()
.observeOnUI()
.subscribe(
{ upgradeInfo ->
successOnce = true
processUpdateInfo(upgradeInfo)
},
{
currentState = State.error(it)
}
)
}
private fun processUpdateInfo(upgradeInfo: UpgradeInfo) {
currentState = State.success(upgradeInfo)
if (upgradeInfo.isNewVersion) {
AppUpgradeChecker.upgradeInfo = upgradeInfo
safeContext {
upgradeInteractor.showUpgradeDialog(it, upgradeInfo,
onCancel = {
//do nothing
},
onConfirm = {
doUpdate()
}
)
}
}
}
private fun doUpdate() {
//show loading dialog
safeContext {
upgradeInteractor.showDownloadingDialog(it, upgradeInfo.isForce)
}
//register result receiver
var receiver = resultReceiver
if (receiver == null) {
receiver = ResultBroadcastReceiver()
resultReceiver = receiver
LocalBroadcastManager.getInstance(BaseUtils.getAppContext()).registerReceiver(receiver, IntentFilter(UpgradeService.DOWNLOAD_APK_RESULT_ACTION))
}
//start downloading
isDownloading = true
UpgradeService.start(BaseUtils.getAppContext(), upgradeInfo.downloadUrl, upgradeInfo.versionName, upgradeInfo.digitalAbstract, upgradeInfo.isForce)
}
private class ResultBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != UpgradeService.DOWNLOAD_APK_RESULT_ACTION) {
return
}
when (intent.getIntExtra(UpgradeService.NOTIFY_TYPE_KEY, -1)) {
UpgradeService.NOTIFY_TYPE_SUCCESS -> processOnDownloadingFileSuccessful(intent)
UpgradeService.NOTIFY_TYPE_FAILED -> processOnDownloadingFileFailed()
UpgradeService.NOTIFY_TYPE_PROGRESS -> {
val total = intent.getLongExtra(UpgradeService.TOTAL_PROGRESS_KEY, 0)
val progress = intent.getLongExtra(UpgradeService.PROGRESS_KEY, 0)
upgradeInteractor.onProgress(total, progress)
}
}
}
}
private fun processOnDownloadingFileSuccessful(intent: Intent) {
isDownloading = false
//start installing
val apkFile = intent.getSerializableExtra(UpgradeService.APK_FILE_KEY) as File
startInstall(apkFile)
//dismiss download dialog
upgradeInteractor.dismissDownloadingDialog()
// if it is force upgrade, we show a no cancelable dialog to make user have to install the new apk.
safeContext {
showInstallTipsDialog(it, apkFile)
}
}
private fun showInstallTipsDialog(topActivity: Activity, apkFile: File) {
upgradeInteractor.showInstallTipsDialog(topActivity, upgradeInfo.isForce,
onCancel = {
currentState = State.success()
},
onConfirm = {
startInstall(apkFile)
})
}
private fun processOnDownloadingFileFailed() {
isDownloading = false
upgradeInteractor.dismissDownloadingDialog()
safeContext {
showDownloadingFailedTips(it, upgradeInfo.isForce)
}
}
private fun showDownloadingFailedTips(safeContext: Activity, isForce: Boolean) {
upgradeInteractor.showDownloadingFailed(safeContext, isForce,
onConfirm = {
doUpdate()
},
onCancel = {
currentState = State.success()
})
}
private fun safeContext(onContext: (Activity) -> Unit) {
val topActivity = Sword.topActivity
if (topActivity != null) {
onContext(topActivity)
} else {
AppUtils.registerAppStatusChangedListener(this, object : Utils.OnAppStatusChangedListener {
override fun onBackground() = Unit
override fun onForeground() {
AppUtils.unregisterAppStatusChangedListener(this)
Sword.topActivity?.let { onContext(it) }
}
})
}
}
private fun startInstall(apkFile: File) {
upgradeInteractor.installApk(apkFile, upgradeInfo)
}
}

@ -1,16 +0,0 @@
package com.android.base.utils.upgrade
/**
*@author Ztiany
* Email: ztiany3@gmail.com
* Date : 2019-10-29 19:17
*/
data class UpgradeInfo(
val isNewVersion: Boolean,
val isForce: Boolean,
val versionName: String,
val downloadUrl: String,
val description: String,
val digitalAbstract: String,
val raw: Any?
)

@ -1,35 +0,0 @@
package com.app.base.upgrade
import android.content.Context
import com.android.base.utils.upgrade.UpgradeInfo
import io.reactivex.Flowable
import java.io.File
/**
*@author Ztiany
* Email: ztiany3@gmail.com
* Date : 2019-10-29 19:16
*/
interface UpgradeInteractor {
fun checkUpgrade(): Flowable<UpgradeInfo>
fun showUpgradeDialog(context: Context, upgradeInfo: UpgradeInfo, onCancel: () -> Unit, onConfirm: () -> Unit)
fun showInstallTipsDialog(context: Context, force: Boolean, onCancel: () -> Unit, onConfirm: () -> Unit)
fun showDownloadingFailed(context: Context, force: Boolean, onCancel: () -> Unit, onConfirm: () -> Unit)
fun showDownloadingDialog(context: Context, force: Boolean)
fun dismissDownloadingDialog()
fun onProgress(total: Long, progress: Long)
fun installApk(file: File, upgradeInfo: UpgradeInfo)
fun checkApkFile(apkFile: File, digitalAbstract: String): Boolean
fun generateAppDownloadPath(versionName: String): String
}

@ -1,110 +0,0 @@
package com.android.base.utils.upgrade;
import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import com.android.base.utils.BaseUtils;
import com.android.base.utils.common.Strings;
import java.io.File;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import timber.log.Timber;
/**
* 下载服务不要配置在子进程
*/
public class UpgradeService extends IntentService {
static final String DOWNLOAD_APK_RESULT_ACTION = BaseUtils.getAppContext().getPackageName() + ".upgrade.result";
static final String IS_FORCE = "is_force";
static final String NOTIFY_TYPE_KEY = "notify_type";
static final String APK_FILE_KEY = "apk_path_key";
static final String URL_KEY = "url_key";
static final String VERSION_KEY = "version_key";
static final String TOTAL_PROGRESS_KEY = "total_progress_key";
static final String PROGRESS_KEY = "progress_key";
static final String DIGITAL_ABSTRACT_KEY = "digital_abstract_key";
public static final int NOTIFY_TYPE_FAILED = 1;
public static final int NOTIFY_TYPE_PROGRESS = 2;
public static final int NOTIFY_TYPE_SUCCESS = 3;
private Handler mHandler;
private boolean mIsForceUpdate;
private String mDigitalAbstract;
private class FlagDownloaderListener implements ApkDownloader.ApkDownloaderListener {
@Override
public void onProgress(long total, long progress) {
Timber.d("onProgress() called with: total = [" + total + "], progress = [" + progress + "]");
mHandler.post(() -> notifyDownloadResult(NOTIFY_TYPE_PROGRESS, null, total, progress));
}
@Override
public void onSuccess(File desFile) {
Timber.d("onSuccess() called with: desFile = [" + desFile + "]");
mHandler.post(() -> notifyDownloadResult(NOTIFY_TYPE_SUCCESS, desFile, 0, 0));
}
@Override
public void onFail(Exception e) {
Timber.d("onFail() called with: e = [" + e + "]");
mHandler.post(() -> notifyDownloadResult(NOTIFY_TYPE_FAILED, null, 0, 0));
}
}
public UpgradeService() {
super("GW-UpgradeService");
}
public static void start(Context context, String updateUrl, String versionName, String digitalAbstract, boolean isForceUpdate) {
if (Strings.isEmpty(updateUrl) || Strings.isEmpty(versionName)) {
return;
}
Intent intent = new Intent(context, UpgradeService.class);
intent.putExtra(URL_KEY, updateUrl);
intent.putExtra(IS_FORCE, isForceUpdate);
intent.putExtra(VERSION_KEY, versionName);
intent.putExtra(DIGITAL_ABSTRACT_KEY, digitalAbstract);
context.startService(intent);
}
@Override
protected void onHandleIntent(Intent intent) {
String url = intent.getStringExtra(URL_KEY);
String versionName = intent.getStringExtra(VERSION_KEY);
mIsForceUpdate = intent.getBooleanExtra(IS_FORCE, false);
mDigitalAbstract = intent.getStringExtra(DIGITAL_ABSTRACT_KEY);
if (!TextUtils.isEmpty(url) && !TextUtils.isEmpty(versionName)) {
new ApkDownloader(url, AppUpgradeChecker.upgradeInteractor.generateAppDownloadPath(versionName), mDigitalAbstract, new FlagDownloaderListener()).start();
}
}
@Override
public void onCreate() {
super.onCreate();
mHandler = new Handler(Looper.getMainLooper());
}
private void notifyDownloadResult(int notifyType, File desFile, long total, long progress) {
Intent intent = new Intent();
intent.setAction(DOWNLOAD_APK_RESULT_ACTION);
intent.putExtra(IS_FORCE, mIsForceUpdate);
intent.putExtra(NOTIFY_TYPE_KEY, notifyType);
intent.putExtra(TOTAL_PROGRESS_KEY, total);
intent.putExtra(PROGRESS_KEY, progress);
if (desFile != null) {
intent.putExtra(APK_FILE_KEY, desFile);
}
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
}
}

@ -0,0 +1,18 @@
package com.android.base.widget.shape
import androidx.annotation.IntDef
import com.android.base.widget.shape.Direction.Companion.BOTTOM
import com.android.base.widget.shape.Direction.Companion.LEFT
import com.android.base.widget.shape.Direction.Companion.RIGHT
import com.android.base.widget.shape.Direction.Companion.TOP
@IntDef(TOP, BOTTOM, LEFT, RIGHT)
@Retention(AnnotationRetention.SOURCE)
annotation class Direction {
companion object {
const val TOP = 1
const val BOTTOM = 2
const val LEFT = 3
const val RIGHT = 4
}
}

@ -0,0 +1,208 @@
package com.android.base.widget.shape
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Path
import android.util.AttributeSet
import android.widget.TextView
import com.android.base.R
import com.android.base.utils.android.views.use
/**
*@author Ztiany
* Email: ztiany3@gmail.com
* Date : 2019-01-16 10:09
*/
class TriangleView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : TextView(context, attrs, defStyleAttr) {
private val mPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG)
@Suppress var bottomColor: Int = 0
set(value) {
field = value
invalidate()
}
var strokeColor: Int = 0
set(value) {
field = value
invalidate()
}
@Suppress var triangleSolidColor: Int = 0
set(value) {
field = value
invalidate()
}
var strokeWidth: Float = 0F
set(value) {
field = value
invalidate()
}
@Direction var direction: Int = 0
set(value) {
field = value
invalidate()
}
var trianglePercent: Float = 0F
set(value) {
field = value
invalidate()
}
private val mTrianglePath: Path = Path()
init {
initAttribute(context, attrs)
}
@SuppressLint("Recycle")
private fun initAttribute(context: Context, attrs: AttributeSet?) {
context.obtainStyledAttributes(attrs, R.styleable.TriangleView).use {
strokeColor = it.getColor(R.styleable.TriangleView_tv_triangle_stroke_color, Color.BLACK)
bottomColor = it.getColor(R.styleable.TriangleView_tv_triangle_bottom_color, -10)
if (bottomColor == -10) {
bottomColor = strokeColor
}
triangleSolidColor = it.getColor(R.styleable.TriangleView_tv_triangle_solid_color, Color.TRANSPARENT)
strokeWidth = it.getDimension(R.styleable.TriangleView_tv_triangle_stroke_width, 0F)
direction = it.getInt(R.styleable.TriangleView_tv_triangle_direction, Direction.TOP)
trianglePercent = it.getFloat(R.styleable.TriangleView_tv_triangle_angle_percent, 0.5F)
}
mPaint.strokeWidth = strokeWidth
mPaint.strokeCap = Paint.Cap.ROUND
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (measuredHeight <= 0 || measuredWidth <= 0) {
return
}
when (direction) {
Direction.TOP -> drawTopTriangle(canvas)
Direction.BOTTOM -> drawBottomTriangle(canvas)
Direction.LEFT -> drawLeftTriangle(canvas)
Direction.RIGHT -> drawRightTriangle(canvas)
}
}
private fun drawLeftTriangle(canvas: Canvas) {
val trianglePoint = trianglePercent * measuredHeight
val drawingHeight = measuredHeight.toFloat()
val drawingWidth = measuredWidth.toFloat()
val haftStroke = strokeWidth * 0.5F
//draw solid
if (needDrawSolid()) {
mTrianglePath.reset()
mTrianglePath.moveTo(drawingWidth, 0F)
mTrianglePath.lineTo(drawingWidth, drawingHeight)
mTrianglePath.lineTo(0F, trianglePoint)
mTrianglePath.close()
mPaint.style = Paint.Style.FILL
mPaint.color = triangleSolidColor
canvas.drawPath(mTrianglePath, mPaint)
}
//draw stroke
if (strokeWidth != 0F) {
mPaint.color = strokeColor
canvas.drawLine(0F, trianglePoint, drawingWidth, 0F, mPaint)
canvas.drawLine(0F, trianglePoint, drawingWidth, drawingHeight, mPaint)
mPaint.color = bottomColor
canvas.drawLine(drawingWidth - haftStroke, 0F, drawingWidth - haftStroke, drawingHeight, mPaint)
}
}
private fun drawRightTriangle(canvas: Canvas) {
val trianglePoint = trianglePercent * measuredHeight
val drawingHeight = measuredHeight.toFloat()
val drawingWidth = measuredWidth.toFloat()
val haftStroke = strokeWidth * 0.5F
//draw solid
if (needDrawSolid()) {
mTrianglePath.reset()
mTrianglePath.moveTo(0F, 0F)
mTrianglePath.lineTo(0F, drawingHeight)
mTrianglePath.lineTo(drawingWidth, trianglePoint)
mTrianglePath.close()
mPaint.style = Paint.Style.FILL
mPaint.color = triangleSolidColor
canvas.drawPath(mTrianglePath, mPaint)
}
//draw stroke
if (strokeWidth != 0F) {
mPaint.color = strokeColor
canvas.drawLine(0F, 0F, drawingWidth, trianglePoint, mPaint)
canvas.drawLine(drawingWidth, trianglePoint, 0F, drawingHeight, mPaint)
mPaint.color = bottomColor
canvas.drawLine(haftStroke, 0F, haftStroke, drawingHeight, mPaint)
}
}
private fun drawBottomTriangle(canvas: Canvas) {
val trianglePoint = trianglePercent * measuredWidth
val drawingHeight = measuredHeight.toFloat()
val drawingWidth = measuredWidth.toFloat()
val haftStroke = strokeWidth * 0.5F
//draw solid
if (needDrawSolid()) {
mTrianglePath.reset()
mTrianglePath.moveTo(0F, 0F)
mTrianglePath.lineTo(trianglePoint, drawingHeight)
mTrianglePath.lineTo(drawingWidth, 0F)
mTrianglePath.close()
mPaint.style = Paint.Style.FILL
mPaint.color = triangleSolidColor
canvas.drawPath(mTrianglePath, mPaint)
}
//draw stroke
if (strokeWidth != 0F) {
mPaint.color = strokeColor
canvas.drawLine(0F, 0F, trianglePoint, drawingHeight, mPaint)
canvas.drawLine(trianglePoint, drawingHeight, drawingWidth, 0F, mPaint)
mPaint.color = bottomColor
canvas.drawLine(0F, haftStroke, drawingWidth, haftStroke, mPaint)
}
}
private fun drawTopTriangle(canvas: Canvas) {
val trianglePoint = trianglePercent * measuredWidth
val drawingHeight = measuredHeight.toFloat()
val drawingWidth = measuredWidth.toFloat()
val haftStroke = strokeWidth * 0.5F
//draw solid
if (needDrawSolid()) {
mTrianglePath.reset()
mTrianglePath.moveTo(0F, drawingHeight)
mTrianglePath.lineTo(drawingWidth, drawingHeight)
mTrianglePath.lineTo(trianglePoint, 0F)
mTrianglePath.close()
mPaint.style = Paint.Style.FILL
mPaint.color = triangleSolidColor
canvas.drawPath(mTrianglePath, mPaint)
}
//draw stroke
if (strokeWidth != 0F) {
mPaint.color = strokeColor
canvas.drawLine(0F, drawingHeight, trianglePoint, 0F, mPaint)
canvas.drawLine(trianglePoint, 0F, drawingWidth, drawingHeight, mPaint)
mPaint.color = bottomColor
canvas.drawLine(drawingWidth, drawingHeight - haftStroke, 0F, drawingHeight - haftStroke, mPaint)
}
}
private fun needDrawSolid() = triangleSolidColor != Color.TRANSPARENT
}

@ -82,4 +82,28 @@
<attr name="psv_zoom_factory" format="float"/>
</declare-styleable>
<declare-styleable name="TriangleView">
<attr name="tv_triangle_solid_color"
format="color|reference" />
<attr name="tv_triangle_bottom_color"
format="color|reference" />
<attr name="tv_triangle_stroke_color"
format="color|reference" />
<attr name="tv_triangle_stroke_width"
format="dimension|reference" />
<attr name="tv_triangle_angle_percent"
format="float" />
<attr name="tv_triangle_direction"
format="enum">
<enum name="top"
value="1" />
<enum name="down"
value="2" />
<enum name="left"
value="3" />
<enum name="right"
value="4" />
</attr>
</declare-styleable>
</resources>

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id=":lib_coroutines" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/../../AndroidArchitecture" external.system.id="GRADLE" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android-gradle" name="Android-Gradle">
<configuration>
<option name="GRADLE_PROJECT_PATH" value=":lib_coroutines" />
<option name="LAST_SUCCESSFUL_SYNC_AGP_VERSION" />
<option name="LAST_KNOWN_AGP_VERSION" />
</configuration>
</facet>
<facet type="java-gradle" name="Java-Gradle">
<configuration>
<option name="BUILD_FOLDER_PATH" value="$MODULE_DIR$/build" />
<option name="BUILDABLE" value="false" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.gradle" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
Loading…
Cancel
Save