Browse Source

Create: audio track widget module

liweihao 6 years ago
commit
9a2491806b

+ 53 - 0
.gitignore

@@ -0,0 +1,53 @@
+# Built application files
+*.apk
+*.ap_
+
+# Files for the ART/Dalvik VM
+*.dex
+
+# Java class files
+*.class
+
+# Generated files
+bin/
+gen/
+out/
+
+# Gradle files
+.gradle/
+build/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Proguard folder generated by Eclipse
+proguard/
+
+# Log Files
+*.log
+
+# Android Studio Navigation editor temp files
+.navigation/
+
+# Android Studio captures folder
+captures/
+
+# Intellij
+*.iml
+.idea/
+
+# Keystore files
+*.jks
+
+# Windows thumbnail db
+Thumbs.db
+
+# OSX files
+.DS_Store
+
+# Eclipse project files
+.classpath
+.project
+
+keystore.properties
+*.keystore

+ 38 - 0
build.gradle

@@ -0,0 +1,38 @@
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+    compileSdkVersion 28
+
+
+
+    defaultConfig {
+        minSdkVersion 21
+        targetSdkVersion 28
+        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 "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+    implementation 'com.android.support:appcompat-v7:28.0.0-rc01'
+    testImplementation 'junit:junit:4.12'
+    androidTestImplementation 'com.android.support.test:runner:1.0.2'
+    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
+}
+repositories {
+    mavenCentral()
+}

+ 21 - 0
proguard-rules.pro

@@ -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

+ 26 - 0
src/androidTest/java/com/example/audiotrack/ExampleInstrumentedTest.java

@@ -0,0 +1,26 @@
+package com.example.audiotrack;
+
+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.example.audiotrack.test", appContext.getPackageName());
+    }
+}

+ 6 - 0
src/main/AndroidManifest.xml

@@ -0,0 +1,6 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.audiotrack" >
+
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+
+</manifest>

+ 185 - 0
src/main/java/com/example/audiotrack/AudioTrackView.kt

@@ -0,0 +1,185 @@
+package com.example.audiotrack
+
+import android.content.Context
+import android.graphics.*
+import android.graphics.drawable.Drawable
+import android.support.v4.content.ContextCompat
+import android.support.v4.view.GestureDetectorCompat
+import android.util.AttributeSet
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.view.View
+
+abstract class AudioTrackView : View, GestureDetector.OnGestureListener {
+    interface OnScrollListener {
+        fun onScroll(scrollRatio: Double)
+    }
+
+    var outerLeft: Double = 0.0
+    var outerRight: Double = width.toDouble()
+
+    var startX: Double = 0.0
+    var startY: Double = 0.0
+
+    var innerWidthRatio: Float = 0f
+    var innerWidth: Double = 0.0
+    var innerColor: Int = 0
+
+    var drawInnerColor:Int = 0
+
+    var endDrawable: Drawable? = null
+
+    var scrollable: Boolean = false
+    internal var onScrollListener: OnScrollListener? = null
+
+    protected val paint = Paint()
+
+    private val body: RectF = RectF()
+
+    private val gestureDetectorCompat = GestureDetectorCompat(context, this)
+
+
+    constructor(context: Context) : super(context)
+
+    constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) {
+        initAttr(attributeSet)
+    }
+
+    constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr) {
+        initAttr(attributeSet)
+    }
+
+    private fun initAttr(attributeSet: AttributeSet) {
+        val typedArray = context.theme.obtainStyledAttributes(attributeSet, R.styleable.AudioTrackView, 0, 0)
+        innerWidthRatio = typedArray.getFloat(R.styleable.AudioTrackView_innerWidthRatio, 0f)
+        innerColor = typedArray.getColor(R.styleable.AudioTrackView_innerColor, ContextCompat.getColor(context, android.R.color.white))
+        endDrawable = typedArray.getDrawable(R.styleable.AudioTrackView_endDrawable)
+
+        drawInnerColor = innerColor
+    }
+
+    init {
+        paint.isAntiAlias = true
+    }
+
+    fun setOnScrollsListener(onScrollListener: OnScrollListener) {
+        this.onScrollListener = onScrollListener
+    }
+
+    override fun onDraw(canvas: Canvas?) {
+        super.onDraw(canvas)
+
+        canvas?.let {
+            drawBackground(canvas, outerLeft.toInt(), outerRight.toInt())
+            drawBody(canvas, startX, startY)
+        }
+    }
+
+    open fun drawBackground(canvas: Canvas, left: Int, right: Int) {
+        background.bounds = Rect(left, 0, right, height)
+        background.draw(canvas)
+    }
+
+    private fun drawBody(canvas: Canvas, drawX: Double, drawY: Double) {
+        val drawEndX = if ((drawX + innerWidth) > outerRight) outerRight else drawX + innerWidth
+        val drawEndY = drawY + height
+
+        paint.color = drawInnerColor
+        body.set(drawX.toFloat(), drawY.toFloat(), drawEndX.toFloat(), drawEndY.toFloat())
+        canvas.drawRect(body, paint)
+    }
+
+    open fun drawEnd(canvas: Canvas, drawX: Double, drawY: Double, height: Int) {
+        endDrawable?.let {
+            val drawCenterX = if ((drawX + innerWidth) > outerRight) outerRight else drawX + innerWidth
+            val drawCenterY = drawY + height / 2
+            val drawRadius = it.intrinsicWidth / 2
+            val shadowRange = resources.getDimension(R.dimen.shadow_range)
+
+            paint.color = ContextCompat.getColor(context, R.color.shadow_layer2)
+            canvas.drawCircle(drawCenterX.toFloat(),
+                    (drawCenterY + 2 * shadowRange).toFloat(),
+                    drawRadius + 2 * shadowRange,
+                    paint)
+
+            paint.color = ContextCompat.getColor(context, R.color.shadow_layer1)
+            canvas.drawCircle(drawCenterX.toFloat(),
+                    (drawCenterY + shadowRange).toFloat(),
+                    drawRadius + shadowRange,
+                    paint)
+
+            paint.color = ContextCompat.getColor(context, android.R.color.white)
+            canvas.drawCircle(drawCenterX.toFloat(), drawCenterY.toFloat(), drawRadius.toFloat(), paint)
+
+            it.bounds = Rect(
+                    (drawCenterX - drawRadius).toInt(),
+                    (drawCenterY - drawRadius).toInt(),
+                    (drawCenterX + drawRadius).toInt(),
+                    (drawCenterY + drawRadius).toInt())
+            it.draw(canvas)
+        }
+    }
+
+    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+        super.onSizeChanged(w, h, oldw, oldh)
+
+        endDrawable?.let {
+            outerLeft = (it.intrinsicWidth / 2).toDouble()
+            outerRight = (width - it.intrinsicWidth / 2).toDouble()
+        }
+
+        startX = outerLeft
+
+        innerWidth = (innerWidthRatio * (outerRight - outerLeft))
+    }
+
+    override fun onTouchEvent(event: MotionEvent?): Boolean {
+        if (gestureDetectorCompat.onTouchEvent(event)) {
+            return true
+        }
+        return super.onTouchEvent(event)
+    }
+
+    override fun onDown(p0: MotionEvent?): Boolean {
+        var endX = startX + innerWidth
+        val endY = startY + height
+
+        endDrawable?.let {
+            endX -= it.intrinsicWidth / 2
+        }
+
+        p0?.let {
+            return (it.x in startX..endX) && (it.y in startY..endY)
+        }
+        return false
+    }
+
+    override fun onScroll(p0: MotionEvent?, p1: MotionEvent?, p2: Float, p3: Float): Boolean {
+        if (scrollable) {
+
+            startX = if (startX - p2 < outerLeft) outerLeft
+            else if (startX - p2 > (outerRight - innerWidth)) outerRight - innerWidth
+            else startX - p2
+
+            onScrollListener?.onScroll((startX - outerLeft) / (outerRight - outerLeft))
+
+            invalidate()
+            return true
+        }
+        return false
+    }
+
+    override fun onSingleTapUp(p0: MotionEvent?): Boolean {
+        return true
+    }
+
+    override fun onShowPress(p0: MotionEvent?) {
+    }
+
+    override fun onFling(p0: MotionEvent?, p1: MotionEvent?, p2: Float, p3: Float): Boolean {
+        return false
+    }
+
+    override fun onLongPress(p0: MotionEvent?) {
+    }
+}

+ 344 - 0
src/main/java/com/example/audiotrack/EditAudioTrackView.kt

@@ -0,0 +1,344 @@
+package com.example.audiotrack
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.os.Build
+import android.support.v4.content.ContextCompat
+import android.text.Layout
+import android.text.StaticLayout
+import android.text.TextPaint
+import android.text.TextUtils
+import android.util.AttributeSet
+import android.view.MotionEvent
+
+class EditAudioTrackView : AudioTrackView {
+    interface OnEditScrollListener {
+        fun onScroll(scrollRatio: Double)
+    }
+
+    var headDrawable: Drawable? = null
+
+    var innerHeight: Int = 0
+
+    var dialogTextSize: Int = 0
+
+    var playProgress: Float = 0f
+    var playProgressText: String = ""
+    var playProgressDialogColor: Int = 0
+
+    var scrollProgressText: String = ""
+    var scrollProgressDialogColor: Int = 0
+
+    private var playX: Double = outerLeft
+
+    private val textPaint = TextPaint()
+
+    private var isTouchHeadDrawable: Boolean = false
+    private var isTouchEndDrawable: Boolean = false
+    private var onHeadScrollListener: OnEditScrollListener? = null
+    private var onEndScrollListener: OnEditScrollListener? = null
+
+    constructor(context: Context) : super(context)
+
+    constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) {
+        initAttr(attributeSet)
+    }
+
+    constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr) {
+        initAttr(attributeSet)
+    }
+
+    private fun initAttr(attributeSet: AttributeSet) {
+        val typedArray = context.theme.obtainStyledAttributes(attributeSet, R.styleable.EditAudioTrackView, 0, 0)
+        headDrawable = typedArray.getDrawable(R.styleable.EditAudioTrackView_headDrawable)
+        innerHeight = typedArray.getDimensionPixelSize(R.styleable.EditAudioTrackView_innerHeight, 0)
+        playProgress = typedArray.getFloat(R.styleable.EditAudioTrackView_playProgress, 0f)
+        playProgressText = typedArray.getString(R.styleable.EditAudioTrackView_playProgressText)
+        playProgressDialogColor = typedArray.getColor(R.styleable.EditAudioTrackView_playProgressDialogColor, ContextCompat.getColor(context, android.R.color.white))
+        scrollProgressText = typedArray.getString(R.styleable.EditAudioTrackView_scrollProgressText)
+        scrollProgressDialogColor = typedArray.getColor(R.styleable.EditAudioTrackView_scrollProgressDialogColor, ContextCompat.getColor(context, android.R.color.white))
+        dialogTextSize = typedArray.getDimensionPixelSize(R.styleable.EditAudioTrackView_dialogTextSize, resources.getDimensionPixelSize(R.dimen.dialog_text_size))
+    }
+
+    init {
+        scrollable = false
+    }
+
+    fun setOnHeadScrollListener(onHeadScrollListener: OnEditScrollListener) {
+        this.onHeadScrollListener = onHeadScrollListener
+    }
+
+    fun setOnEndScrollListener(onEndScrollListener: OnEditScrollListener) {
+        this.onEndScrollListener = onEndScrollListener
+    }
+
+    override fun onDraw(canvas: Canvas?) {
+        super.onDraw(canvas)
+
+        canvas?.let {
+            drawHead(canvas, startX, startY)
+            drawEnd(canvas, startX, startY, innerHeight)
+
+            drawTimerDialog(playX.toFloat(),
+                    ((resources.getDimensionPixelSize(R.dimen.dialog_margin_bottom_height) - resources.getDimensionPixelSize(R.dimen.dialog_margin_bottom_low)).toFloat()),
+                    resources.getDimensionPixelSize(R.dimen.dialog_margin_bottom_low),
+                    playProgressDialogColor,
+                    playProgressText,
+                    canvas)
+
+            if (isTouchHeadDrawable) {
+                drawTimerDialog(startX.toFloat(),
+                        0f,
+                        resources.getDimensionPixelSize(R.dimen.dialog_margin_bottom_height),
+                        scrollProgressDialogColor,
+                        scrollProgressText,
+                        canvas
+                )
+            }
+
+            if (isTouchEndDrawable) {
+                drawTimerDialog((startX + innerWidth).toFloat(),
+                        0f,
+                        resources.getDimensionPixelSize(R.dimen.dialog_margin_bottom_height),
+                        scrollProgressDialogColor,
+                        scrollProgressText,
+                        canvas
+                )
+            }
+        }
+    }
+
+    override fun drawBackground(canvas: Canvas, left: Int, right: Int) {
+        val backgroundStartY = (resources.getDimensionPixelSize(R.dimen.dialog_height) + resources.getDimensionPixelSize(R.dimen.dialog_margin_bottom_height))
+
+        background.bounds = Rect(left,
+                backgroundStartY,
+                right,
+                (backgroundStartY + innerHeight))
+        background.draw(canvas)
+    }
+
+    private fun drawHead(canvas: Canvas, drawX: Double, drawY: Double) {
+        headDrawable?.let {
+            val drawCenterY = drawY + innerHeight / 2
+            val drawRadius = it.intrinsicWidth / 2
+            val shadowRange = resources.getDimension(R.dimen.shadow_range)
+
+            paint.color = ContextCompat.getColor(context, R.color.shadow_layer2)
+            canvas.drawCircle(drawX.toFloat(),
+                    (drawCenterY + 2 * shadowRange).toFloat(),
+                    drawRadius + 2 * shadowRange,
+                    paint)
+
+            paint.color = ContextCompat.getColor(context, R.color.shadow_layer1)
+            canvas.drawCircle(drawX.toFloat(),
+                    (drawCenterY + shadowRange).toFloat(),
+                    drawRadius + shadowRange,
+                    paint)
+
+            paint.color = ContextCompat.getColor(context, android.R.color.white)
+            canvas.drawCircle(drawX.toFloat(), drawCenterY.toFloat(), drawRadius.toFloat(), paint)
+
+            it.bounds = Rect(
+                    (drawX - drawRadius).toInt(),
+                    (drawCenterY - drawRadius).toInt(),
+                    (drawX + drawRadius).toInt(),
+                    (drawCenterY + drawRadius).toInt())
+            it.draw(canvas)
+        }
+    }
+
+    private fun drawTimerDialog(startX: Float, startY: Float, marginBottom: Int, color: Int, text: String, canvas: Canvas) {
+        paint.color = color
+        textPaint.color = ContextCompat.getColor(context, android.R.color.black)
+        textPaint.isAntiAlias = true
+        textPaint.textSize = dialogTextSize.toFloat()
+
+        val staticLayout: StaticLayout = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            val staticLayoutBuilder = StaticLayout.Builder.obtain(text,
+                    0,
+                    text.length,
+                    textPaint,
+                    resources.getDimensionPixelSize(R.dimen.dialog_text_width))
+            staticLayoutBuilder.setEllipsizedWidth(resources.getDimensionPixelSize(R.dimen.dialog_width))
+            staticLayoutBuilder.setEllipsize(TextUtils.TruncateAt.END)
+            staticLayoutBuilder.setMaxLines(1)
+            staticLayoutBuilder.build()
+        } else {
+            StaticLayout(text,
+                    0,
+                    text.length,
+                    textPaint,
+                    resources.getDimensionPixelSize(R.dimen.dialog_text_width),
+                    Layout.Alignment.ALIGN_NORMAL,
+                    0f,
+                    0f,
+                    true,
+                    TextUtils.TruncateAt.END,
+                    resources.getDimensionPixelSize(R.dimen.dialog_width))
+        }
+
+        canvas.drawLine(startX,
+                startY + resources.getDimensionPixelSize(R.dimen.dialog_height),
+                startX + resources.getDimensionPixelSize(R.dimen.dialog_pointer_width),
+                startY + resources.getDimensionPixelSize(R.dimen.dialog_height) + marginBottom + height,
+                paint
+        )
+
+        canvas.drawRoundRect(startX - resources.getDimensionPixelSize(R.dimen.dialog_width) / 2,
+                startY,
+                startX + resources.getDimensionPixelSize(R.dimen.dialog_width) / 2,
+                startY + resources.getDimensionPixelSize(R.dimen.dialog_height),
+                resources.getDimensionPixelSize(R.dimen.dialog_round_radius).toFloat(),
+                resources.getDimensionPixelSize(R.dimen.dialog_round_radius).toFloat(),
+                paint
+        )
+
+        canvas.save()
+        canvas.translate(startX - staticLayout.width / 2, startY + resources.getDimensionPixelSize(R.dimen.dialog_height) / 2 - staticLayout.height / 2)
+        staticLayout.draw(canvas)
+        canvas.restore()
+    }
+
+    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+        var height = height
+
+        when (MeasureSpec.getMode(heightMeasureSpec)) {
+            MeasureSpec.AT_MOST, MeasureSpec.UNSPECIFIED -> {
+                height = (innerHeight
+                        + resources.getDimensionPixelSize(R.dimen.dialog_height)
+                        + resources.getDimensionPixelSize(R.dimen.dialog_margin_bottom_height))
+            }
+            MeasureSpec.EXACTLY -> {
+            }
+        }
+        setMeasuredDimension(width, height)
+    }
+
+    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+        super.onSizeChanged(w, h, oldw, oldh)
+
+        endDrawable?.let {
+            outerLeft = (resources.getDimensionPixelSize(R.dimen.dialog_width) / 2).toDouble()
+            outerRight = (width - (resources.getDimensionPixelSize(R.dimen.dialog_width) / 2)).toDouble()
+        }
+
+        startX = outerLeft
+        startY = ((resources.getDimensionPixelSize(R.dimen.dialog_height)
+                + resources.getDimensionPixelSize(R.dimen.dialog_margin_bottom_height)).toDouble())
+
+        innerWidth = (innerWidthRatio * (outerRight - outerLeft))
+        playX = (outerRight - outerLeft) * playProgress
+    }
+
+    override fun onTouchEvent(event: MotionEvent?): Boolean {
+        when (event?.actionMasked) {
+            MotionEvent.ACTION_UP -> {
+                isTouchEndDrawable = false
+                isTouchHeadDrawable = false
+                invalidate()
+            }
+        }
+        return super.onTouchEvent(event)
+    }
+
+    override fun onDown(p0: MotionEvent?): Boolean {
+        return onDownHeadDrawable(p0) || onDownEndDrawable(p0) || super.onDown(p0)
+    }
+
+    private fun onDownHeadDrawable(p0: MotionEvent?): Boolean {
+        var drawableStartX = startX
+        var drawableEndX = startX
+
+        headDrawable?.let {
+            drawableStartX = startX - it.intrinsicWidth / 2 - resources.getDimensionPixelSize(R.dimen.extra_touch_event_range)
+            drawableEndX = startX + it.intrinsicWidth / 2 + resources.getDimensionPixelSize(R.dimen.extra_touch_event_range)
+        }
+
+        p0?.let {
+            if ((it.x in drawableStartX..drawableEndX) && (it.y in startY..(startY + innerHeight))) {
+                return true
+            }
+        }
+        return false
+    }
+
+    private fun onDownEndDrawable(p0: MotionEvent?): Boolean {
+        val drawableX = if (startX + innerWidth > outerRight) outerRight else startX + innerWidth
+        var drawableStartX = drawableX
+        var drawableEndX = drawableX
+
+        endDrawable?.let {
+            drawableStartX = drawableX - it.intrinsicWidth / 2 - resources.getDimensionPixelSize(R.dimen.extra_touch_event_range)
+            drawableEndX = drawableX + it.intrinsicWidth / 2 + resources.getDimensionPixelSize(R.dimen.extra_touch_event_range)
+        }
+
+        p0?.let {
+            if ((it.x in drawableStartX..drawableEndX) && (it.y in startY..(startY + innerHeight))) {
+                return true
+            }
+        }
+        return false
+    }
+
+    override fun onScroll(p0: MotionEvent?, p1: MotionEvent?, p2: Float, p3: Float): Boolean {
+        isTouchHeadDrawable = onScrollHeadDrawable(p1, p2)
+        isTouchEndDrawable = onScrollEndDrawable(p1, p2)
+        return isTouchHeadDrawable || isTouchEndDrawable || super.onScroll(p0, p1, p2, p3)
+    }
+
+    private fun onScrollHeadDrawable(p1: MotionEvent?, p2: Float): Boolean {
+        var drawableStartX = startX
+        var drawableEndX = startX
+
+        headDrawable?.let {
+            drawableStartX = startX - it.intrinsicWidth / 2 - resources.getDimensionPixelSize(R.dimen.extra_touch_event_range)
+            drawableEndX = startX + it.intrinsicWidth / 2 + resources.getDimensionPixelSize(R.dimen.extra_touch_event_range)
+        }
+
+        p1?.let {
+            if ((it.x in drawableStartX..drawableEndX) && (it.y in startY..(startY + innerHeight))) {
+                if (startX - p2 in outerLeft..(startX + innerWidth)) {
+                    innerWidth = (innerWidth + p2)
+                    startX -= p2
+                }
+
+                onHeadScrollListener?.onScroll((startX - outerLeft) / (outerRight - outerLeft))
+
+                invalidate()
+
+                return true
+            }
+        }
+        return false
+    }
+
+    private fun onScrollEndDrawable(p1: MotionEvent?, p2: Float): Boolean {
+        val drawableX = if (startX + innerWidth > outerRight) outerRight else startX + innerWidth
+        var drawableStartX = drawableX
+        var drawableEndX = drawableX
+        endDrawable?.let {
+            drawableStartX = drawableX - it.intrinsicWidth / 2 - resources.getDimensionPixelSize(R.dimen.extra_touch_event_range)
+            drawableEndX = drawableX + it.intrinsicWidth / 2 + resources.getDimensionPixelSize(R.dimen.extra_touch_event_range)
+        }
+
+        p1?.let {
+            if ((it.x in drawableStartX..drawableEndX) && (it.y in startY..(startY + innerHeight))) {
+                if ((startX + innerWidth - p2) in startX..outerRight) {
+                    innerWidth = (innerWidth - p2)
+                }
+
+                onEndScrollListener?.onScroll((startX + innerWidth - outerLeft) / (outerRight - outerLeft))
+
+                invalidate()
+
+                return true
+
+            }
+        }
+        return false
+    }
+}

+ 199 - 0
src/main/java/com/example/audiotrack/LoopAudioTrackView.kt

@@ -0,0 +1,199 @@
+package com.example.audiotrack
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.RectF
+import android.os.Build
+import android.support.v4.content.ContextCompat
+import android.text.Layout
+import android.text.StaticLayout
+import android.text.TextPaint
+import android.text.TextUtils
+import android.util.AttributeSet
+import android.view.MotionEvent
+
+class LoopAudioTrackView : AudioTrackView {
+    interface OnLoopScrollListener {
+        fun onScroll(scrollRatio: Double)
+    }
+
+    var innerText: String = ""
+    var innerTextSize: Int = 0
+    var innerTextPadding: Int = 0
+
+    var isSelect: Boolean = false
+
+    var isLoop: Boolean = false
+    private var loopWidth: Int = 0
+    private var onLoopScrollListener: OnLoopScrollListener? = null
+
+    private val loop: RectF = RectF()
+
+    private val textPaint = TextPaint()
+
+    companion object {
+        const val PAINT_MAX_ALPHA = 255
+    }
+
+    constructor(context: Context) : super(context)
+
+    constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet) {
+        initAttr(attributeSet)
+    }
+
+    constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr) {
+        initAttr(attributeSet)
+    }
+
+    private fun initAttr(attributeSet: AttributeSet) {
+        val typedArray = context.theme.obtainStyledAttributes(attributeSet, R.styleable.LoopAudioTrackView, 0, 0)
+        innerText = typedArray.getString(R.styleable.LoopAudioTrackView_innerText)
+        innerTextSize = typedArray.getDimensionPixelSize(R.styleable.LoopAudioTrackView_innerTextSize, resources.getDimensionPixelSize(R.dimen.default_inner_text_size))
+        innerTextPadding = typedArray.getDimensionPixelSize(R.styleable.LoopAudioTrackView_innerTextPadding, 0)
+        isLoop = typedArray.getBoolean(R.styleable.LoopAudioTrackView_isLoop, false)
+    }
+
+    init {
+        scrollable = true
+    }
+
+    fun setOnLoopScrollListener(onLoopScrollListener: OnLoopScrollListener) {
+        this.onLoopScrollListener = onLoopScrollListener
+    }
+
+    override fun onDraw(canvas: Canvas?) {
+        drawInnerColor = if (isSelect) innerColor else ContextCompat.getColor(context, android.R.color.darker_gray)
+
+        super.onDraw(canvas)
+
+        canvas?.let {
+            drawText(canvas, startX, startY)
+
+            if (isLoop) {
+                drawLoop(canvas, startX, startY)
+                drawEnd(canvas, startX + loopWidth, startY, height)
+            }
+        }
+    }
+
+    private fun drawText(canvas: Canvas, drawX: Double, drawY: Double) {
+        var ellipsizedWidth = innerWidth
+        endDrawable?.let {
+            ellipsizedWidth = innerWidth - it.intrinsicWidth / 2
+        }
+
+        textPaint.isAntiAlias = true
+        textPaint.textSize = innerTextSize.toFloat()
+        textPaint.color = ContextCompat.getColor(context, android.R.color.black)
+
+        val staticLayout: StaticLayout = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            val staticLayoutBuilder = StaticLayout.Builder.obtain(innerText, 0, innerText.length, textPaint, innerWidth.toInt())
+            staticLayoutBuilder.setEllipsizedWidth(ellipsizedWidth.toInt())
+            staticLayoutBuilder.setEllipsize(TextUtils.TruncateAt.END)
+            staticLayoutBuilder.setMaxLines(1)
+            staticLayoutBuilder.build()
+        } else {
+            StaticLayout(innerText,
+                    0,
+                    innerText.length,
+                    textPaint,
+                    innerWidth.toInt(),
+                    Layout.Alignment.ALIGN_NORMAL,
+                    0f,
+                    0f,
+                    true,
+                    TextUtils.TruncateAt.END,
+                    ellipsizedWidth.toInt())
+        }
+
+        if (staticLayout.width >= resources.getDimensionPixelSize(R.dimen.inner_text_display_min_width)) {
+            val drawTextX = drawX + innerTextPadding
+            val drawTextY = drawY + height / 2 - staticLayout.height / 2
+
+            canvas.save()
+            canvas.translate(drawTextX.toFloat(), drawTextY.toFloat())
+            staticLayout.draw(canvas)
+            canvas.restore()
+        }
+    }
+
+    private fun drawLoop(canvas: Canvas, drawX: Double, drawY: Double) {
+        val drawStartX = drawX + innerWidth
+        val drawEndX = if (drawStartX + loopWidth > outerRight) outerRight else drawStartX + loopWidth
+
+        if (drawStartX < drawEndX) {
+            paint.color = drawInnerColor
+            paint.alpha = (PAINT_MAX_ALPHA * .6).toInt()
+            loop.set(drawStartX.toFloat(), drawY.toFloat(), drawEndX.toFloat(), (drawY + height).toFloat())
+            canvas.drawRect(loop, paint)
+        }
+    }
+
+    override fun onDown(p0: MotionEvent?): Boolean {
+        return (isLoop && onDownLoopTrack(p0)) || super.onDown(p0)
+    }
+
+    private fun onDownLoopTrack(p0: MotionEvent?): Boolean {
+        endDrawable?.let {
+            val loopStartX = startX + innerWidth
+            val loopEndX = (startX + innerWidth + loopWidth + it.intrinsicWidth / 2
+                    + resources.getDimensionPixelSize(R.dimen.extra_touch_event_range))
+
+            p0?.let {
+                return ((it.x in loopStartX..loopEndX) && (it.y in startY..(startY + height)))
+            }
+        }
+        return false
+    }
+
+    override fun onSingleTapUp(p0: MotionEvent?): Boolean {
+        isSelect = isSelect.not()
+        invalidate()
+        return super.onSingleTapUp(p0)
+    }
+
+    override fun onScroll(p0: MotionEvent?, p1: MotionEvent?, p2: Float, p3: Float): Boolean {
+        return isSelect && ((isLoop && (onScrollEndDrawable(p1, p2) || onScrollLoopTrack(p2))) || super.onScroll(p0, p1, p2, p3))
+    }
+
+    private fun onScrollEndDrawable(p1: MotionEvent?, p2: Float): Boolean {
+        val drawableX = if (startX + innerWidth + loopWidth > outerRight) outerRight else startX + innerWidth + loopWidth
+        var drawableStartX = 0.0
+        var drawableEndX = 0.0
+
+        endDrawable?.let {
+            drawableStartX = (drawableX - it.intrinsicWidth / 2 - resources.getDimensionPixelSize(R.dimen.extra_touch_event_range))
+            drawableEndX = (drawableX + it.intrinsicWidth / 2 + resources.getDimensionPixelSize(R.dimen.extra_touch_event_range))
+        }
+
+        p1?.let {
+            if ((it.x in drawableStartX..drawableEndX) && (it.y in startY..(startY + height))) {
+                loopWidth = if (loopWidth - p2 < 0) 0
+                else if (loopWidth - p2 > (outerRight - (startX + innerWidth))) ((outerRight - (startX + innerWidth)).toInt())
+                else (loopWidth - p2).toInt()
+
+                onLoopScrollListener?.onScroll(loopWidth / (outerRight - outerLeft))
+
+                invalidate()
+
+                return true
+            }
+        }
+        return false
+    }
+
+    private fun onScrollLoopTrack(p2: Float): Boolean {
+        if (loopWidth > 0) {
+            startX = if (startX - p2 < outerLeft) outerLeft
+            else if (startX - p2 > (outerRight - innerWidth - loopWidth)) outerRight - innerWidth - loopWidth
+            else startX - p2
+
+            onScrollListener?.onScroll((startX - outerLeft) / (outerRight - outerLeft))
+
+            invalidate()
+
+            return true
+        }
+        return false
+    }
+}

+ 6 - 0
src/main/res/drawable/ic_arrow_left_b.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<rotate xmlns:android="http://schemas.android.com/apk/res/android"
+    android:drawable="@drawable/ic_arrow_right_b"
+    android:fromDegrees="180"
+    android:toDegrees="180">
+</rotate>

+ 10 - 0
src/main/res/drawable/ic_arrow_right_b.xml

@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24">
+  <path
+      android:pathData="M10,17l5,-5 -5,-5z"
+      android:fillColor="#000"
+      android:fillType="nonZero"/>
+</vector>

+ 28 - 0
src/main/res/values/attrs.xml

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <declare-styleable name="AudioTrackView">
+        <attr name="innerWidthRatio" format="float"/>
+        <attr name="innerColor" format="color" />
+        <attr name="endDrawable" format="reference" />
+    </declare-styleable>
+
+    <declare-styleable name="LoopAudioTrackView">
+        <attr name="innerText" format="string" />
+        <attr name="innerTextSize" format="dimension" />
+        <attr name="innerTextPadding" format="dimension" />
+        <attr name="isLoop" format="boolean"/>
+        <attr name="isSelect" format="boolean"/>
+    </declare-styleable>
+
+    <declare-styleable name="EditAudioTrackView">
+        <attr name="innerHeight" format="dimension" />
+        <attr name="headDrawable" format="reference" />
+        <attr name="playProgress" format="float"/>
+        <attr name="playProgressText" format="string"/>
+        <attr name="playProgressDialogColor" format="color"/>
+        <attr name="scrollProgressText" format="string"/>
+        <attr name="scrollProgressDialogColor" format="color"/>
+        <attr name="dialogTextSize" format="dimension"/>
+    </declare-styleable>
+
+</resources>

+ 5 - 0
src/main/res/values/colors.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="shadow_layer1">#0E000000</color>
+    <color name="shadow_layer2">#04000000</color>
+</resources>

+ 17 - 0
src/main/res/values/dimens.xml

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <dimen name="shadow_range">1dp</dimen>
+    <dimen name="inner_text_display_min_width">8dp</dimen>
+    <dimen name="default_inner_text_size">14sp</dimen>
+    <dimen name="extra_touch_event_range">16dp</dimen>
+    
+    <!-- timer dialog -->
+    <dimen name="dialog_pointer_width">1dp</dimen>
+    <dimen name="dialog_width">65dp</dimen>
+    <dimen name="dialog_height">28dp</dimen>
+    <dimen name="dialog_round_radius">12dp</dimen>
+    <dimen name="dialog_margin_bottom_low">4dp</dimen>
+    <dimen name="dialog_margin_bottom_height">16dp</dimen>
+    <dimen name="dialog_text_width">32dp</dimen>
+    <dimen name="dialog_text_size">16sp</dimen>
+</resources>

+ 3 - 0
src/main/res/values/strings.xml

@@ -0,0 +1,3 @@
+<resources>
+    <string name="app_name">audioTrack</string>
+</resources>

+ 17 - 0
src/test/java/com/example/audiotrack/ExampleUnitTest.java

@@ -0,0 +1,17 @@
+package com.example.audiotrack;
+
+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);
+    }
+}