Updates edittext views with corner rounded
This commit is contained in:
parent
7a42723b9e
commit
f9f30c8387
46 changed files with 1127 additions and 1286 deletions
1
library/.gitignore
vendored
Normal file
1
library/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/build
|
41
library/build.gradle
Normal file
41
library/build.gradle
Normal file
|
@ -0,0 +1,41 @@
|
|||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion 27
|
||||
|
||||
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 27
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
implementation 'com.android.support:appcompat-v7:27.1.1'
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'com.android.support.test:runner:1.0.1'
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
|
||||
implementation "androidx.core:core-ktx:+"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
21
library/proguard-rules.pro
vendored
Normal file
21
library/proguard-rules.pro
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
|
@ -0,0 +1,26 @@
|
|||
package com.lauzy.freedom.library;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExampleInstrumentedTest {
|
||||
@Test
|
||||
public void useAppContext() {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getTargetContext();
|
||||
|
||||
assertEquals("com.lauzy.freedom.library.test", appContext.getPackageName());
|
||||
}
|
||||
}
|
2
library/src/main/AndroidManifest.xml
Normal file
2
library/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,2 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.lauzy.freedom.library"/>
|
30
library/src/main/java/com/lauzy/freedom/library/Lrc.java
Normal file
30
library/src/main/java/com/lauzy/freedom/library/Lrc.java
Normal file
|
@ -0,0 +1,30 @@
|
|||
package com.lauzy.freedom.library;
|
||||
|
||||
/**
|
||||
* Desc : 歌词实体
|
||||
* Author : Lauzy
|
||||
* Date : 2017/10/13
|
||||
* Blog : http://www.jianshu.com/u/e76853f863a9
|
||||
* Email : freedompaladin@gmail.com
|
||||
*/
|
||||
public class Lrc {
|
||||
private long time;
|
||||
private String text;
|
||||
|
||||
public void setTime(long time) {
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
public long getTime() {
|
||||
return time;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
}
|
137
library/src/main/java/com/lauzy/freedom/library/LrcHelper.java
Normal file
137
library/src/main/java/com/lauzy/freedom/library/LrcHelper.java
Normal file
|
@ -0,0 +1,137 @@
|
|||
package com.lauzy.freedom.library;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Desc : 歌词解析
|
||||
* Author : Lauzy
|
||||
* Date : 2017/10/13
|
||||
* Blog : http://www.jianshu.com/u/e76853f863a9
|
||||
* Email : freedompaladin@gmail.com
|
||||
*/
|
||||
public class LrcHelper {
|
||||
|
||||
private static final String CHARSET = "utf-8";
|
||||
//[03:56.00][03:18.00][02:06.00][01:07.00]原谅我这一生不羁放纵爱自由
|
||||
private static final String LINE_REGEX = "((\\[\\d{2}:\\d{2}\\.\\d{2}])+)(.*)";
|
||||
private static final String TIME_REGEX = "\\[(\\d{2}):(\\d{2})\\.(\\d{2})]";
|
||||
|
||||
public static List<Lrc> parseLrcFromAssets(Context context, String fileName) {
|
||||
try {
|
||||
return parseInputStream(context.getResources().getAssets().open(fileName));
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static List<Lrc> parseLrcFromFile(File file) {
|
||||
try {
|
||||
return parseInputStream(new FileInputStream(file));
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static List<Lrc> parseInputStream(InputStream inputStream) {
|
||||
List<Lrc> lrcs = new ArrayList<>();
|
||||
InputStreamReader isr = null;
|
||||
BufferedReader br = null;
|
||||
try {
|
||||
isr = new InputStreamReader(inputStream, CHARSET);
|
||||
br = new BufferedReader(isr);
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
List<Lrc> lrcList = parseLrc(line);
|
||||
if (lrcList != null && lrcList.size() != 0) {
|
||||
lrcs.addAll(lrcList);
|
||||
}
|
||||
}
|
||||
sortLrcs(lrcs);
|
||||
return lrcs;
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
e.printStackTrace();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
try {
|
||||
if (isr != null) {
|
||||
isr.close();
|
||||
}
|
||||
if (br != null) {
|
||||
br.close();
|
||||
}
|
||||
} catch (IOException e1) {
|
||||
e1.printStackTrace();
|
||||
}
|
||||
}
|
||||
return lrcs;
|
||||
}
|
||||
|
||||
private static void sortLrcs(List<Lrc> lrcs) {
|
||||
Collections.sort(lrcs, new Comparator<Lrc>() {
|
||||
@Override
|
||||
public int compare(Lrc o1, Lrc o2) {
|
||||
return (int) (o1.getTime() - o2.getTime());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static List<Lrc> parseLrc(String lrcLine) {
|
||||
if (lrcLine.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
List<Lrc> lrcs = new ArrayList<>();
|
||||
Matcher matcher = Pattern.compile(LINE_REGEX).matcher(lrcLine);
|
||||
if (!matcher.matches()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String time = matcher.group(1);
|
||||
String content = matcher.group(3);
|
||||
Matcher timeMatcher = Pattern.compile(TIME_REGEX).matcher(time);
|
||||
|
||||
while (timeMatcher.find()) {
|
||||
String min = timeMatcher.group(1);
|
||||
String sec = timeMatcher.group(2);
|
||||
String mil = timeMatcher.group(3);
|
||||
Lrc lrc = new Lrc();
|
||||
if (content != null && content.length() != 0) {
|
||||
lrc.setTime(Long.parseLong(min) * 60 * 1000 + Long.parseLong(sec) * 1000
|
||||
+ Long.parseLong(mil) * 10);
|
||||
lrc.setText(content);
|
||||
lrcs.add(lrc);
|
||||
}
|
||||
}
|
||||
return lrcs;
|
||||
}
|
||||
|
||||
public static String formatTime(long time) {
|
||||
int min = (int) (time / 60000);
|
||||
int sec = (int) (time / 1000 % 60);
|
||||
return adjustFormat(min) + ":" + adjustFormat(sec);
|
||||
}
|
||||
|
||||
private static String adjustFormat(int time) {
|
||||
if (time < 10) {
|
||||
return "0" + time;
|
||||
}
|
||||
return time + "";
|
||||
}
|
||||
}
|
620
library/src/main/java/com/lauzy/freedom/library/LrcView.kt
Normal file
620
library/src/main/java/com/lauzy/freedom/library/LrcView.kt
Normal file
|
@ -0,0 +1,620 @@
|
|||
package com.lauzy.freedom.library
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Looper
|
||||
import android.text.Layout
|
||||
import android.text.StaticLayout
|
||||
import android.text.TextPaint
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.view.MotionEvent
|
||||
import android.view.VelocityTracker
|
||||
import android.view.View
|
||||
import android.view.ViewConfiguration
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.widget.OverScroller
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.core.view.ViewCompat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Desc : 歌词
|
||||
* Author : Lauzy
|
||||
* Date : 2017/10/13
|
||||
* Blog : http://www.jianshu.com/u/e76853f863a9
|
||||
* Email : freedompaladin@gmail.com
|
||||
*/
|
||||
class LrcView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) {
|
||||
private var mLrcData: MutableList<Lrc>? = null
|
||||
private var mTextPaint: TextPaint? = null
|
||||
private var mDefaultContent: String? = null
|
||||
private var mCurrentLine: Int = 0
|
||||
private var mOffset: Float = 0.toFloat()
|
||||
private var mLastMotionX: Float = 0.toFloat()
|
||||
private var mLastMotionY: Float = 0.toFloat()
|
||||
private var mScaledTouchSlop: Int = 0
|
||||
private var mOverScroller: OverScroller? = null
|
||||
private var mVelocityTracker: VelocityTracker? = null
|
||||
private var mMaximumFlingVelocity: Int = 0
|
||||
private var mMinimumFlingVelocity: Int = 0
|
||||
private var mLrcTextSize: Float = 0.toFloat()
|
||||
private var mLrcLineSpaceHeight: Float = 0.toFloat()
|
||||
private var mTouchDelay: Int = 0
|
||||
private var mNormalColor: Int = 0
|
||||
private var mCurrentPlayLineColor: Int = 0
|
||||
private var mNoLrcTextSize: Float = 0.toFloat()
|
||||
private var mNoLrcTextColor: Int = 0
|
||||
//是否拖拽中,否的话响应onClick事件
|
||||
private var isDragging: Boolean = false
|
||||
//用户开始操作
|
||||
private var isUserScroll: Boolean = false
|
||||
private var isAutoAdjustPosition = true
|
||||
private var mPlayDrawable: Drawable? = null
|
||||
private var isShowTimeIndicator: Boolean = false
|
||||
private var mPlayRect: Rect? = null
|
||||
private var mIndicatorPaint: Paint? = null
|
||||
private var mIndicatorLineWidth: Float = 0.toFloat()
|
||||
private var mIndicatorTextSize: Float = 0.toFloat()
|
||||
private var mCurrentIndicateLineTextColor: Int = 0
|
||||
private var mIndicatorLineColor: Int = 0
|
||||
private var mIndicatorMargin: Float = 0.toFloat()
|
||||
private var mIconLineGap: Float = 0.toFloat()
|
||||
private var mIconWidth: Float = 0.toFloat()
|
||||
private var mIconHeight: Float = 0.toFloat()
|
||||
private var isEnableShowIndicator = true
|
||||
private var mIndicatorTextColor: Int = 0
|
||||
private var mIndicatorTouchDelay: Int = 0
|
||||
private val mLrcMap = HashMap<String, StaticLayout>()
|
||||
private val mHideIndicatorRunnable = Runnable {
|
||||
isShowTimeIndicator = false
|
||||
invalidateView()
|
||||
}
|
||||
private val mStaticLayoutHashMap = HashMap<String, StaticLayout>()
|
||||
private val mScrollRunnable = Runnable {
|
||||
isUserScroll = false
|
||||
scrollToPosition(mCurrentLine)
|
||||
}
|
||||
private var mOnPlayIndicatorLineListener: OnPlayIndicatorLineListener? = null
|
||||
|
||||
private val lrcWidth: Int
|
||||
get() = width - paddingLeft - paddingRight
|
||||
|
||||
private val lrcHeight: Int
|
||||
get() = height
|
||||
|
||||
private val isLrcEmpty: Boolean
|
||||
get() = mLrcData == null || lrcCount == 0
|
||||
|
||||
private val lrcCount: Int
|
||||
get() = mLrcData!!.size
|
||||
|
||||
//itemOffset 和 mOffset 最小即当前位置
|
||||
val indicatePosition: Int
|
||||
get() {
|
||||
var pos = 0
|
||||
var min = java.lang.Float.MAX_VALUE
|
||||
for (i in mLrcData!!.indices) {
|
||||
val offsetY = getItemOffsetY(i)
|
||||
val abs = Math.abs(offsetY - mOffset)
|
||||
if (abs < min) {
|
||||
min = abs
|
||||
pos = i
|
||||
}
|
||||
}
|
||||
return pos
|
||||
}
|
||||
|
||||
var playDrawable: Drawable?
|
||||
get() = mPlayDrawable
|
||||
set(playDrawable) {
|
||||
mPlayDrawable = playDrawable
|
||||
mPlayDrawable!!.bounds = mPlayRect!!
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
init {
|
||||
init(context, attrs)
|
||||
}
|
||||
|
||||
private fun init(context: Context, attrs: AttributeSet?) {
|
||||
|
||||
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.LrcView)
|
||||
mLrcTextSize = typedArray.getDimension(R.styleable.LrcView_lrcTextSize, sp2px(context, 15f).toFloat())
|
||||
mLrcLineSpaceHeight = typedArray.getDimension(R.styleable.LrcView_lrcLineSpaceSize, dp2px(context, 20f).toFloat())
|
||||
mTouchDelay = typedArray.getInt(R.styleable.LrcView_lrcTouchDelay, 3500)
|
||||
mIndicatorTouchDelay = typedArray.getInt(R.styleable.LrcView_indicatorTouchDelay, 2500)
|
||||
mNormalColor = typedArray.getColor(R.styleable.LrcView_lrcNormalTextColor, Color.GRAY)
|
||||
mCurrentPlayLineColor = typedArray.getColor(R.styleable.LrcView_lrcCurrentTextColor, Color.BLUE)
|
||||
mNoLrcTextSize = typedArray.getDimension(R.styleable.LrcView_noLrcTextSize, dp2px(context, 20f).toFloat())
|
||||
mNoLrcTextColor = typedArray.getColor(R.styleable.LrcView_noLrcTextColor, Color.BLACK)
|
||||
mIndicatorLineWidth = typedArray.getDimension(R.styleable.LrcView_indicatorLineHeight, dp2px(context, 0.5f).toFloat())
|
||||
mIndicatorTextSize = typedArray.getDimension(R.styleable.LrcView_indicatorTextSize, sp2px(context, 13f).toFloat())
|
||||
mIndicatorTextColor = typedArray.getColor(R.styleable.LrcView_indicatorTextColor, Color.GRAY)
|
||||
mCurrentIndicateLineTextColor = typedArray.getColor(R.styleable.LrcView_currentIndicateLrcColor, Color.GRAY)
|
||||
mIndicatorLineColor = typedArray.getColor(R.styleable.LrcView_indicatorLineColor, Color.GRAY)
|
||||
mIndicatorMargin = typedArray.getDimension(R.styleable.LrcView_indicatorStartEndMargin, dp2px(context, 5f).toFloat())
|
||||
mIconLineGap = typedArray.getDimension(R.styleable.LrcView_iconLineGap, dp2px(context, 3f).toFloat())
|
||||
mIconWidth = typedArray.getDimension(R.styleable.LrcView_playIconWidth, dp2px(context, 20f).toFloat())
|
||||
mIconHeight = typedArray.getDimension(R.styleable.LrcView_playIconHeight, dp2px(context, 20f).toFloat())
|
||||
mPlayDrawable = typedArray.getDrawable(R.styleable.LrcView_playIcon)
|
||||
mPlayDrawable = if (mPlayDrawable == null) ContextCompat.getDrawable(context, R.drawable.play_icon) else mPlayDrawable
|
||||
typedArray.recycle()
|
||||
|
||||
setupConfigs(context)
|
||||
}
|
||||
|
||||
private fun setupConfigs(context: Context) {
|
||||
mScaledTouchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
||||
mMaximumFlingVelocity = ViewConfiguration.get(context).scaledMaximumFlingVelocity
|
||||
mMinimumFlingVelocity = ViewConfiguration.get(context).scaledMinimumFlingVelocity
|
||||
mOverScroller = OverScroller(context, DecelerateInterpolator())
|
||||
mOverScroller!!.setFriction(0.1f)
|
||||
// ViewConfiguration.getScrollFriction(); 默认摩擦力 0.015f
|
||||
|
||||
mTextPaint = TextPaint()
|
||||
mTextPaint!!.apply {
|
||||
isAntiAlias = true
|
||||
textAlign = Paint.Align.LEFT
|
||||
textSize = mLrcTextSize
|
||||
typeface = ResourcesCompat.getFont(context, R.font.circular)
|
||||
}
|
||||
mDefaultContent = DEFAULT_CONTENT
|
||||
|
||||
mIndicatorPaint = Paint()
|
||||
mIndicatorPaint!!.isAntiAlias = true
|
||||
mIndicatorPaint!!.strokeWidth = mIndicatorLineWidth
|
||||
mIndicatorPaint!!.color = mIndicatorLineColor
|
||||
mPlayRect = Rect()
|
||||
mIndicatorPaint!!.textSize = mIndicatorTextSize
|
||||
}
|
||||
|
||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||
super.onLayout(changed, left, top, right, bottom)
|
||||
if (changed) {
|
||||
mPlayRect!!.left = mIndicatorMargin.toInt()
|
||||
mPlayRect!!.top = (height / 2 - mIconHeight / 2).toInt()
|
||||
mPlayRect!!.right = (mPlayRect!!.left + mIconWidth).toInt()
|
||||
mPlayRect!!.bottom = (mPlayRect!!.top + mIconHeight).toInt()
|
||||
mPlayDrawable!!.bounds = mPlayRect!!
|
||||
}
|
||||
}
|
||||
|
||||
fun setLrcData(lrcData: MutableList<Lrc>) {
|
||||
resetView(DEFAULT_CONTENT)
|
||||
mLrcData = lrcData
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
if (isLrcEmpty) {
|
||||
drawEmptyText(canvas)
|
||||
return
|
||||
}
|
||||
val indicatePosition = indicatePosition
|
||||
mTextPaint!!.textSize = mLrcTextSize
|
||||
mTextPaint!!.textAlign = Paint.Align.LEFT
|
||||
var y = (lrcHeight / 2).toFloat()
|
||||
val x = dip2px(context, 16f).toFloat()
|
||||
for (i in 0 until lrcCount) {
|
||||
if (i > 0) {
|
||||
y += (getTextHeight(i - 1) + getTextHeight(i)) / 2 + mLrcLineSpaceHeight
|
||||
}
|
||||
if (mCurrentLine == i) {
|
||||
mTextPaint!!.color = mCurrentPlayLineColor
|
||||
} else if (indicatePosition == i && isShowTimeIndicator) {
|
||||
mTextPaint!!.color = mCurrentIndicateLineTextColor
|
||||
} else {
|
||||
mTextPaint!!.color = mNormalColor
|
||||
}
|
||||
drawLrc(canvas, x, y, i)
|
||||
}
|
||||
|
||||
if (isShowTimeIndicator) {
|
||||
mPlayDrawable!!.draw(canvas)
|
||||
val time = mLrcData!![indicatePosition].time
|
||||
val timeWidth = mIndicatorPaint!!.measureText(LrcHelper.formatTime(time))
|
||||
mIndicatorPaint!!.color = mIndicatorLineColor
|
||||
canvas.drawLine(mPlayRect!!.right + mIconLineGap, (height / 2).toFloat(),
|
||||
width - timeWidth * 1.3f, (height / 2).toFloat(), mIndicatorPaint!!)
|
||||
val baseX = (width - timeWidth * 1.1f).toInt()
|
||||
val baseline = (height / 2).toFloat() - (mIndicatorPaint!!.descent() - mIndicatorPaint!!.ascent()) / 2 - mIndicatorPaint!!.ascent()
|
||||
mIndicatorPaint!!.color = mIndicatorTextColor
|
||||
canvas.drawText(LrcHelper.formatTime(time), baseX.toFloat(), baseline, mIndicatorPaint!!)
|
||||
}
|
||||
}
|
||||
|
||||
private fun dip2px(context: Context, dpVale: Float): Int {
|
||||
val scale = context.resources.displayMetrics.density
|
||||
return (dpVale * scale + 0.5f).toInt()
|
||||
}
|
||||
|
||||
private fun drawLrc(canvas: Canvas, x: Float, y: Float, i: Int) {
|
||||
val text = mLrcData!![i].text
|
||||
var staticLayout: StaticLayout? = mLrcMap[text]
|
||||
if (staticLayout == null) {
|
||||
mTextPaint!!.textSize = mLrcTextSize
|
||||
staticLayout = StaticLayout(text, mTextPaint, lrcWidth, Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false)
|
||||
mLrcMap[text] = staticLayout
|
||||
}
|
||||
canvas.save()
|
||||
canvas.translate(x, y - (staticLayout.height / 2).toFloat() - mOffset)
|
||||
staticLayout.draw(canvas)
|
||||
canvas.restore()
|
||||
}
|
||||
|
||||
//中间空文字
|
||||
private fun drawEmptyText(canvas: Canvas) {
|
||||
mTextPaint!!.textAlign = Paint.Align.LEFT
|
||||
mTextPaint!!.color = mNoLrcTextColor
|
||||
mTextPaint!!.textSize = mNoLrcTextSize
|
||||
canvas.save()
|
||||
val staticLayout = StaticLayout(mDefaultContent, mTextPaint,
|
||||
lrcWidth, Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false)
|
||||
val margin = dip2px(context, 16f).toFloat();
|
||||
canvas.translate(margin, margin)
|
||||
staticLayout.draw(canvas)
|
||||
canvas.restore()
|
||||
}
|
||||
|
||||
fun updateTime(time: Long) {
|
||||
if (isLrcEmpty) {
|
||||
return
|
||||
}
|
||||
val linePosition = getUpdateTimeLinePosition(time)
|
||||
if (mCurrentLine != linePosition) {
|
||||
mCurrentLine = linePosition
|
||||
if (isUserScroll) {
|
||||
invalidateView()
|
||||
return
|
||||
}
|
||||
ViewCompat.postOnAnimation(this@LrcView, mScrollRunnable)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUpdateTimeLinePosition(time: Long): Int {
|
||||
var linePos = 0
|
||||
for (i in 0 until lrcCount) {
|
||||
val lrc = mLrcData!![i]
|
||||
if (time >= lrc.time) {
|
||||
if (i == lrcCount - 1) {
|
||||
linePos = lrcCount - 1
|
||||
} else if (time < mLrcData!![i + 1].time) {
|
||||
linePos = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return linePos
|
||||
}
|
||||
|
||||
private fun scrollToPosition(linePosition: Int) {
|
||||
val scrollY = getItemOffsetY(linePosition)
|
||||
val animator = ValueAnimator.ofFloat(mOffset, scrollY)
|
||||
animator.addUpdateListener { animation ->
|
||||
mOffset = animation.animatedValue as Float
|
||||
invalidateView()
|
||||
}
|
||||
animator.duration = 300
|
||||
animator.start()
|
||||
}
|
||||
|
||||
private fun getItemOffsetY(linePosition: Int): Float {
|
||||
var tempY = 0f
|
||||
for (i in 1..linePosition) {
|
||||
tempY += (getTextHeight(i - 1) + getTextHeight(i)) / 2 + mLrcLineSpaceHeight
|
||||
}
|
||||
return tempY
|
||||
}
|
||||
|
||||
private fun getTextHeight(linePosition: Int): Float {
|
||||
val text = mLrcData!![linePosition].text
|
||||
var staticLayout: StaticLayout? = mStaticLayoutHashMap[text]
|
||||
if (staticLayout == null) {
|
||||
mTextPaint!!.textSize = mLrcTextSize
|
||||
staticLayout = StaticLayout(text, mTextPaint,
|
||||
lrcWidth, Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false)
|
||||
mStaticLayoutHashMap[text] = staticLayout
|
||||
}
|
||||
return staticLayout.height.toFloat()
|
||||
}
|
||||
|
||||
private fun overScrolled(): Boolean {
|
||||
return mOffset > getItemOffsetY(lrcCount - 1) || mOffset < 0
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
if (isLrcEmpty) {
|
||||
return super.onTouchEvent(event)
|
||||
}
|
||||
if (mVelocityTracker == null) {
|
||||
mVelocityTracker = VelocityTracker.obtain()
|
||||
}
|
||||
mVelocityTracker!!.addMovement(event)
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
removeCallbacks(mScrollRunnable)
|
||||
removeCallbacks(mHideIndicatorRunnable)
|
||||
if (!mOverScroller!!.isFinished) {
|
||||
mOverScroller!!.abortAnimation()
|
||||
}
|
||||
mLastMotionX = event.x
|
||||
mLastMotionY = event.y
|
||||
isUserScroll = true
|
||||
isDragging = false
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
var moveY = event.y - mLastMotionY
|
||||
if (Math.abs(moveY) > mScaledTouchSlop) {
|
||||
isDragging = true
|
||||
isShowTimeIndicator = isEnableShowIndicator
|
||||
}
|
||||
if (isDragging) {
|
||||
|
||||
// if (mOffset < 0) {
|
||||
// mOffset = Math.max(mOffset, -getTextHeight(0) - mLrcLineSpaceHeight);
|
||||
// }
|
||||
val maxHeight = getItemOffsetY(lrcCount - 1)
|
||||
// if (mOffset > maxHeight) {
|
||||
// mOffset = Math.min(mOffset, maxHeight + getTextHeight(getLrcCount() - 1) + mLrcLineSpaceHeight);
|
||||
// }
|
||||
if (mOffset < 0 || mOffset > maxHeight) {
|
||||
moveY /= 3.5f
|
||||
}
|
||||
mOffset -= moveY
|
||||
mLastMotionY = event.y
|
||||
invalidateView()
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> {
|
||||
if (!isDragging && (!isShowTimeIndicator || !onClickPlayButton(event))) {
|
||||
isShowTimeIndicator = false
|
||||
invalidateView()
|
||||
performClick()
|
||||
}
|
||||
handleActionUp(event)
|
||||
}
|
||||
}
|
||||
// return isDragging || super.onTouchEvent(event);
|
||||
return true
|
||||
}
|
||||
|
||||
private fun handleActionUp(event: MotionEvent) {
|
||||
if (isEnableShowIndicator) {
|
||||
ViewCompat.postOnAnimationDelayed(this@LrcView, mHideIndicatorRunnable, mIndicatorTouchDelay.toLong())
|
||||
}
|
||||
if (isShowTimeIndicator && mPlayRect != null && onClickPlayButton(event)) {
|
||||
isShowTimeIndicator = false
|
||||
invalidateView()
|
||||
if (mOnPlayIndicatorLineListener != null) {
|
||||
mOnPlayIndicatorLineListener!!.onPlay(mLrcData!![indicatePosition].time,
|
||||
mLrcData!![indicatePosition].text)
|
||||
}
|
||||
}
|
||||
if (overScrolled() && mOffset < 0) {
|
||||
scrollToPosition(0)
|
||||
if (isAutoAdjustPosition) {
|
||||
ViewCompat.postOnAnimationDelayed(this@LrcView, mScrollRunnable, mTouchDelay.toLong())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (overScrolled() && mOffset > getItemOffsetY(lrcCount - 1)) {
|
||||
scrollToPosition(lrcCount - 1)
|
||||
if (isAutoAdjustPosition) {
|
||||
ViewCompat.postOnAnimationDelayed(this@LrcView, mScrollRunnable, mTouchDelay.toLong())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
mVelocityTracker!!.computeCurrentVelocity(1000, mMaximumFlingVelocity.toFloat())
|
||||
val YVelocity = mVelocityTracker!!.yVelocity
|
||||
val absYVelocity = Math.abs(YVelocity)
|
||||
if (absYVelocity > mMinimumFlingVelocity) {
|
||||
mOverScroller!!.fling(0, mOffset.toInt(), 0, (-YVelocity).toInt(), 0,
|
||||
0, 0, getItemOffsetY(lrcCount - 1).toInt(),
|
||||
0, getTextHeight(0).toInt())
|
||||
invalidateView()
|
||||
}
|
||||
releaseVelocityTracker()
|
||||
if (isAutoAdjustPosition) {
|
||||
ViewCompat.postOnAnimationDelayed(this@LrcView, mScrollRunnable, mTouchDelay.toLong())
|
||||
}
|
||||
}
|
||||
|
||||
private fun onClickPlayButton(event: MotionEvent): Boolean {
|
||||
val left = mPlayRect!!.left.toFloat()
|
||||
val right = mPlayRect!!.right.toFloat()
|
||||
val top = mPlayRect!!.top.toFloat()
|
||||
val bottom = mPlayRect!!.bottom.toFloat()
|
||||
val x = event.x
|
||||
val y = event.y
|
||||
return (mLastMotionX > left && mLastMotionX < right && mLastMotionY > top
|
||||
&& mLastMotionY < bottom && x > left && x < right && y > top && y < bottom)
|
||||
}
|
||||
|
||||
override fun computeScroll() {
|
||||
super.computeScroll()
|
||||
if (mOverScroller!!.computeScrollOffset()) {
|
||||
mOffset = mOverScroller!!.currY.toFloat()
|
||||
invalidateView()
|
||||
}
|
||||
}
|
||||
|
||||
private fun releaseVelocityTracker() {
|
||||
if (null != mVelocityTracker) {
|
||||
mVelocityTracker!!.clear()
|
||||
mVelocityTracker!!.recycle()
|
||||
mVelocityTracker = null
|
||||
}
|
||||
}
|
||||
|
||||
fun resetView(defaultContent: String) {
|
||||
if (mLrcData != null) {
|
||||
mLrcData!!.clear()
|
||||
}
|
||||
mLrcMap.clear()
|
||||
mStaticLayoutHashMap.clear()
|
||||
mCurrentLine = 0
|
||||
mOffset = 0f
|
||||
isUserScroll = false
|
||||
isDragging = false
|
||||
mDefaultContent = defaultContent
|
||||
removeCallbacks(mScrollRunnable)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun performClick(): Boolean {
|
||||
return super.performClick()
|
||||
}
|
||||
|
||||
fun dp2px(context: Context, dpVal: Float): Int {
|
||||
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
|
||||
dpVal, context.resources.displayMetrics).toInt()
|
||||
}
|
||||
|
||||
fun sp2px(context: Context, spVal: Float): Int {
|
||||
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
|
||||
spVal, context.resources.displayMetrics).toInt()
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停(手动滑动歌词后,不再自动回滚至当前播放位置)
|
||||
*/
|
||||
fun pause() {
|
||||
isAutoAdjustPosition = false
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复(继续自动回滚)
|
||||
*/
|
||||
fun resume() {
|
||||
isAutoAdjustPosition = true
|
||||
ViewCompat.postOnAnimationDelayed(this@LrcView, mScrollRunnable, mTouchDelay.toLong())
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
|
||||
/*------------------Config-------------------*/
|
||||
|
||||
private fun invalidateView() {
|
||||
if (Looper.getMainLooper().thread === Thread.currentThread()) {
|
||||
invalidate()
|
||||
} else {
|
||||
postInvalidate()
|
||||
}
|
||||
}
|
||||
|
||||
fun setOnPlayIndicatorLineListener(onPlayIndicatorLineListener: OnPlayIndicatorLineListener) {
|
||||
mOnPlayIndicatorLineListener = onPlayIndicatorLineListener
|
||||
}
|
||||
|
||||
fun setEmptyContent(defaultContent: String) {
|
||||
mDefaultContent = defaultContent
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setLrcTextSize(lrcTextSize: Float) {
|
||||
mLrcTextSize = lrcTextSize
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setLrcLineSpaceHeight(lrcLineSpaceHeight: Float) {
|
||||
mLrcLineSpaceHeight = lrcLineSpaceHeight
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setTouchDelay(touchDelay: Int) {
|
||||
mTouchDelay = touchDelay
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setNormalColor(@ColorInt normalColor: Int) {
|
||||
mNormalColor = normalColor
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setCurrentPlayLineColor(@ColorInt currentPlayLineColor: Int) {
|
||||
mCurrentPlayLineColor = currentPlayLineColor
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setNoLrcTextSize(noLrcTextSize: Float) {
|
||||
mNoLrcTextSize = noLrcTextSize
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setNoLrcTextColor(@ColorInt noLrcTextColor: Int) {
|
||||
mNoLrcTextColor = noLrcTextColor
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setIndicatorLineWidth(indicatorLineWidth: Float) {
|
||||
mIndicatorLineWidth = indicatorLineWidth
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setIndicatorTextSize(indicatorTextSize: Float) {
|
||||
// mIndicatorTextSize = indicatorTextSize;
|
||||
mIndicatorPaint!!.textSize = indicatorTextSize
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setCurrentIndicateLineTextColor(currentIndicateLineTextColor: Int) {
|
||||
mCurrentIndicateLineTextColor = currentIndicateLineTextColor
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setIndicatorLineColor(indicatorLineColor: Int) {
|
||||
mIndicatorLineColor = indicatorLineColor
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setIndicatorMargin(indicatorMargin: Float) {
|
||||
mIndicatorMargin = indicatorMargin
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setIconLineGap(iconLineGap: Float) {
|
||||
mIconLineGap = iconLineGap
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setIconWidth(iconWidth: Float) {
|
||||
mIconWidth = iconWidth
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setIconHeight(iconHeight: Float) {
|
||||
mIconHeight = iconHeight
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setEnableShowIndicator(enableShowIndicator: Boolean) {
|
||||
isEnableShowIndicator = enableShowIndicator
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
fun setIndicatorTextColor(indicatorTextColor: Int) {
|
||||
mIndicatorTextColor = indicatorTextColor
|
||||
invalidateView()
|
||||
}
|
||||
|
||||
interface OnPlayIndicatorLineListener {
|
||||
fun onPlay(time: Long, content: String)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_CONTENT = "Empty"
|
||||
}
|
||||
}
|
BIN
library/src/main/res/drawable-xhdpi/play_icon.png
Normal file
BIN
library/src/main/res/drawable-xhdpi/play_icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 707 B |
11
library/src/main/res/font/circular.xml
Normal file
11
library/src/main/res/font/circular.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<font-family xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<font
|
||||
android:font="@font/circular_std_book"
|
||||
android:fontStyle="normal"
|
||||
android:fontWeight="400" />
|
||||
<font
|
||||
android:font="@font/circular_std_black"
|
||||
android:fontWeight="900" />
|
||||
|
||||
</font-family>
|
BIN
library/src/main/res/font/circular_std_black.otf
Executable file
BIN
library/src/main/res/font/circular_std_black.otf
Executable file
Binary file not shown.
BIN
library/src/main/res/font/circular_std_book.otf
Executable file
BIN
library/src/main/res/font/circular_std_book.otf
Executable file
Binary file not shown.
24
library/src/main/res/values/attrs.xml
Normal file
24
library/src/main/res/values/attrs.xml
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<declare-styleable name="LrcView">
|
||||
<attr name="lrcTextSize" format="dimension"/>
|
||||
<attr name="lrcLineSpaceSize" format="dimension"/>
|
||||
<attr name="lrcNormalTextColor" format="reference|color"/>
|
||||
<attr name="lrcCurrentTextColor" format="reference|color"/>
|
||||
<attr name="lrcTouchDelay" format="integer"/>
|
||||
<attr name="noLrcTextSize" format="dimension"/>
|
||||
<attr name="noLrcTextColor" format="reference|color"/>
|
||||
<attr name="indicatorLineHeight" format="dimension"/>
|
||||
<attr name="indicatorTextSize" format="dimension"/>
|
||||
<attr name="indicatorTextColor" format="reference|color"/>
|
||||
<attr name="currentIndicateLrcColor" format="reference|color"/>
|
||||
<attr name="indicatorTouchDelay" format="integer"/>
|
||||
<attr name="indicatorLineColor" format="reference|color"/>
|
||||
<attr name="indicatorStartEndMargin" format="dimension"/>
|
||||
<attr name="iconLineGap" format="dimension"/>
|
||||
<attr name="playIconWidth" format="dimension"/>
|
||||
<attr name="playIconHeight" format="dimension"/>
|
||||
<attr name="playIcon" format="reference"/>
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
3
library/src/main/res/values/strings.xml
Normal file
3
library/src/main/res/values/strings.xml
Normal file
|
@ -0,0 +1,3 @@
|
|||
<resources>
|
||||
<string name="app_name">library</string>
|
||||
</resources>
|
|
@ -0,0 +1,17 @@
|
|||
package com.lauzy.freedom.library;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Example local unit test, which will execute on the development machine (host).
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
public class ExampleUnitTest {
|
||||
@Test
|
||||
public void addition_isCorrect() {
|
||||
assertEquals(4, 2 + 2);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue