| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- import { LyricLineWrapper } from '../bean/LyricLineWrapper';
- import { printD, printW } from '../extensions/Extension';
- import animator, { AnimatorResult } from '@ohos.animator';
- import { LyricController } from '../LyricController';
- import { Lyric } from '../bean/Lyric';
- /**
- * A component to display the lyric with scroll animation.
- *
- * The custom setter include the lyric text style, fade style of edge, the line space, scroll animation duration,
- * the cache size to draw out of screen.
- */
- @Component
- export struct LyricView {
- /**
- * The lyricConfig for LyricView.
- */
- controller: LyricController = new LyricController()
- private lyricLines = new Array<LyricLineWrapper>() // the lyric lines list.
- readonly settings = new RenderingContextSettings(true)
- readonly canvasCtx = new CanvasRenderingContext2D(this.settings) // paint to draw.
- private highLightTextSize = 0
- private highLightDrawOffset = 0
- private w = 0 // the width of this view.
- private h = 0 // the height of this view.
- private centerH = 0
- private centerW = 0
- private scrollY = 0 // the total scroll offset for current timestamp.
- private linearGradientEdge = 150 // the size of linear gradient at top and bottom.
- private targetScrollY = 0 // the target end scrollY of the animation.
- private centerYOffset = 0 // the center y of the lyric view.
- private currentIndex = 0 // the focused index of the lyric line list.
- private firstVisibleIndex = 0 // the first visible item index at top.
- private lastVisibleIndex = 0 // the last visible item index at bottom.
- private topOverOffIndex = -1 // the index of highlight item when has one more item out off the top.
- private anim: AnimatorResult | null = null
- private isAnimRunning = false
- private isEmpty = false // is no lyric.
- private lyric: Lyric | null = null // current lyric.
- private onDataChangedListener = (lyric: Lyric | null) => {
- this.lyric = lyric
- this.initSize()
- }
- private onPositionChangedListener = (mediaPosition: number) => {
- this.onPositionChanged(mediaPosition)
- }
- private onInvalidatedListener = (reLayout: boolean) => {
- if (reLayout) {
- this.initSize()
- }
- this.initPaint()
- if (reLayout && this.currentIndex > 0) {
- this.animateToIndex(this.currentIndex, true)
- } else {
- this.invalidate()
- }
- }
- aboutToAppear() {
- if (this.controller == null) {
- throw new Error("The lyric lyricConfig is not set!")
- }
- this.controller.onDataChangedListener = this.onDataChangedListener
- this.controller.onPositionChangedListener = this.onPositionChangedListener
- this.controller.onInvalidated = this.onInvalidatedListener
- this.lyric = this.controller.getLyric()
- this.anim = animator.create({
- duration: this.controller.getAnimationDuration(),
- easing: "ease-in-out",
- delay: 0,
- fill: "forwards",
- direction: "normal",
- iterations: 1,
- begin: this.scrollY,
- end: this.targetScrollY
- })
- this.anim.oncancel = () => {
- this.isAnimRunning = false
- }
- this.anim.onfinish = () => {
- this.isAnimRunning = false
- }
- this.anim.onframe = (value) => {
- this.scrollY = value
- this.invalidate()
- }
- }
- build() {
- Canvas(this.canvasCtx)
- .onAreaChange((oldSize, newSize) => {
- if (oldSize.width == newSize.width && oldSize.height == newSize.height) {
- printW("The size is not changed.")
- return
- }
- this.w = newSize.width as number
- this.h = newSize.height as number
- printD("onSizeChanged: canvas size= " + this.w + " * " + this.h)
- this.centerW = this.w / 2
- this.centerH = this.h / 2
- this.initPaint()
- this.initSize()
- })
- .width("100%")
- .height("100%")
- }
- private initPaint() {
- this.canvasCtx.fillStyle = this.controller.getTextColor()
- this.canvasCtx.strokeStyle = this.controller.getTextColor()
- this.canvasCtx.lineWidth = 1
- this.canvasCtx.font = vp2px(this.controller.getTextSize()) + "px"
- // TODO: the center textAlign has bugs when multi line in OpenHarmony 3.2
- if (this.controller.getAlignMode() == "center") {
- this.canvasCtx.textAlign = "center"
- }
- this.highLightTextSize = fp2px(this.controller.getTextSize() * this.controller.getHighlightScale())
- this.highLightDrawOffset = fp2px(this.controller.getTextSize()) * (this.controller.getHighlightScale() - 1) / 2
- }
- private initSize() {
- this.anim!.cancel()
- // reset state
- this.currentIndex = 0
- this.lyricLines = []
- this.isEmpty = this.lyric == null || this.lyric.lyricList.length <= 0
- // no lyric, draw empty hint
- if (this.isEmpty) {
- this.invalidate()
- return
- }
- this.scrollY = this.h / 2 // layout from center when first display
- let tempH = 0
- let list = this.lyric!.lyricList
- for (let i = 0; i < list.length; i++) {
- let lrc = list[i]
- let textMeasure = this.canvasCtx.measureText(lrc.text)
- let lp = textMeasure.width / this.w
- let lines = lp
- if (textMeasure.width % this.w != 0) {
- lines = lp < 1 ? 1 : Math.floor(lp) + 1
- }
- let lineHeight = lines * textMeasure.height
- if (i == 0) {
- tempH = Math.round(textMeasure.height / 2)
- this.centerYOffset = tempH
- }
- if (tempH < this.h / 2) {
- tempH += lineHeight + this.controller.getLineSpace()
- } else {
- if (this.topOverOffIndex == -1) {
- this.topOverOffIndex = i - 1 // if current index is topOverOffIndex, top off screen
- printW("init: topOverIndex= " + this.topOverOffIndex)
- }
- }
- this.lyricLines.push(new LyricLineWrapper(lrc, lineHeight))
- }
- this.firstVisibleIndex = this.getFirstVisibleIndex(this.currentIndex)
- this.lastVisibleIndex = this.getLastVisibleIndex(this.currentIndex)
- this.invalidate()
- }
- private invalidate() {
- // clear canvas
- this.canvasCtx.clearRect(0, 0, this.w, this.h)
- if (!this.isEmpty) {
- // draw lrc text
- this.drawLrc()
- // this.drawEdge()
- } else {
- this.drawEmpty()
- }
- }
- private drawEmpty() {
- let hint = this.controller.getEmptyHint()
- if (this.controller.getAlignMode() == "center") {
- this.canvasCtx.fillText(hint, this.centerW, this.centerH)
- } else {
- this.canvasCtx.fillText(hint, 0, this.centerH)
- }
- }
- private drawLrc() {
- let startY = this.centerYOffset // half of the first line measure text height
- this.canvasCtx.save()
- this.canvasCtx.translate(0, this.scrollY)
- // the draw count is the max visible lines in screen + 4 caches(2 top & 2 bottom)
- for (let i = 0; i < this.lyricLines.length; i++) {
- let lrc = this.lyricLines[i]
- // out the top of screen, the default cache is 2 lines
- let space = this.controller.getLineSpace()
- if (i < this.firstVisibleIndex - this.controller.getCacheSize()) {
- startY += (lrc.height + space)
- continue
- }
- // out the bottom of screen, the default cache is 2 lines
- if (i > this.lastVisibleIndex + this.controller.getCacheSize()) {
- break
- }
- // draw with high light for current line
- if (i == this.currentIndex) {
- let scaleHeight = this.drawHighLight(lrc, startY)
- startY += (scaleHeight + space + this.highLightDrawOffset)
- } else {
- if (this.controller.getAlignMode() == "center") {
- this.canvasCtx.fillText(lrc.text, this.centerW, startY)
- } else {
- this.canvasCtx.fillText(lrc.text, 0, startY)
- }
- startY += (lrc.height + space)
- }
- }
- this.canvasCtx.restore()
- }
- private drawHighLight(lrc: LyricLineWrapper, startY: number): number {
- this.canvasCtx.save()
- this.canvasCtx.font = this.controller.isHighlightBold() ?
- this.highLightTextSize + "px" + " bold" : this.highLightTextSize + "px"
- this.canvasCtx.fillStyle = this.controller.getHighlightColor()
- if (this.controller.getAlignMode() == "center") {
- this.canvasCtx.fillText(lrc.text, this.centerW, startY + this.highLightDrawOffset)
- } else {
- this.canvasCtx.fillText(lrc.text, 0, startY + this.highLightDrawOffset)
- }
- let textMeasure = this.canvasCtx.measureText(lrc.text)
- let height = Math.round(textMeasure.height)
- let lp = textMeasure.width / this.w
- let lines = lp
- if (textMeasure.width % this.w != 0) {
- lines = lp < 1 ? 1 : Math.floor(lp) + 1
- }
- let scaleHeight = height * lines
- this.canvasCtx.restore()
- return scaleHeight
- }
- /**
- * @deprecated since v1.0.2
- */
- private drawEdge() {
- this.canvasCtx.save()
- let topFade = this.canvasCtx.createLinearGradient(this.centerW, 0, this.centerW, this.linearGradientEdge)
- topFade.addColorStop(0.0, this.controller.getEdgeColor())
- topFade.addColorStop(1.0, '#00ffffff')
- this.canvasCtx.fillStyle = topFade
- this.canvasCtx.fillRect(0, 0, this.w, this.linearGradientEdge)
- let bottomFade = this.canvasCtx.createLinearGradient(this.centerW, this.h, this.centerW, this.h - this.linearGradientEdge)
- bottomFade.addColorStop(0.0, this.controller.getEdgeColor())
- bottomFade.addColorStop(1.0, '#00ffffff')
- this.canvasCtx.fillStyle = bottomFade
- this.canvasCtx.fillRect(0, this.h - this.linearGradientEdge, this.w, this.linearGradientEdge)
- this.canvasCtx.restore()
- }
- private getIndex(position: number): number {
- let first = this.lyricLines[0].beginTime
- if (position < first) {
- return 0
- }
- let last = this.lyricLines[this.lyricLines.length - 1].beginTime
- if (position > last) {
- return this.lyricLines.length - 1
- }
- for (let i = 0; i < this.lyricLines.length - 1; i++) {
- let line = this.lyricLines[i]
- if (position >= line.beginTime && position < line.nextTime) {
- return i
- }
- }
- return this.currentIndex
- }
- private getFirstVisibleIndex(index: number): number {
- if (index > this.topOverOffIndex) {
- let tempH = 0
- for (let i = index; i > 0; i--) {
- let lrc = this.lyricLines[i]
- tempH += (lrc.height + this.controller.getLineSpace())
- if (tempH > this.centerH) {
- return i
- }
- }
- }
- return 0
- }
- private getLastVisibleIndex(index: number): number {
- let tempH = 0
- for (let i = index; i < this.lyricLines.length; i++) {
- let lrc = this.lyricLines[i]
- tempH += (lrc.height + this.controller.getLineSpace())
- if (tempH > this.centerH) {
- return i
- }
- }
- return this.lyricLines.length - 1
- }
- private animateToIndex(index: number, focusAnimate: boolean = false) {
- printD("animate to index= " + index)
- if (index == this.currentIndex && !focusAnimate) {
- printD("index not changed, not animate!")
- return
- }
- // out of range, return
- if (index > this.lyricLines.length - 1 || index < 0) {
- printD("out of range, not animate!")
- return
- }
- this.firstVisibleIndex = this.getFirstVisibleIndex(index)
- this.lastVisibleIndex = this.getLastVisibleIndex(index)
- let animOffset = 0
- let isUp = index > this.currentIndex
- if (isUp) { // to next
- for (let i = this.currentIndex; i < index; i++) {
- let lrcH = this.lyricLines[i].height
- animOffset += lrcH + this.controller.getLineSpace()
- }
- } else { // to pre
- for (let i = index; i < this.currentIndex; i++) {
- let lrcH = this.lyricLines[i].height
- animOffset += lrcH + this.controller.getLineSpace()
- }
- }
- this.currentIndex = index
- printD("firstVisible= " + this.firstVisibleIndex + ", lastVisible= " + this.lastVisibleIndex + ", currentHighLight= " +
- this.currentIndex)
- // the animation is running, end it
- if (this.isAnimRunning) {
- this.anim!.cancel()
- this.scrollY = this.targetScrollY
- this.invalidate()
- printW("intercept and end anim!")
- }
- // at end, stop scroll
- if (this.currentIndex > this.lyricLines.length - 1) {
- return
- }
- // get the offset of one frame
- this.targetScrollY = Math.round(isUp ? this.scrollY - animOffset : this.scrollY + animOffset)
- // start animate
- let startY = this.scrollY
- printD(">>start anim " + startY + " -> " + this.targetScrollY)
- this.anim!.reset({
- duration: this.controller.getAnimationDuration(),
- easing: "ease-in-out",
- delay: 0,
- fill: "forwards",
- direction: "normal",
- iterations: 1,
- begin: startY,
- end: this.targetScrollY
- })
- this.anim!.play()
- this.isAnimRunning = true
- }
- private onPositionChanged(mediaPosition: number) {
- if (this.isEmpty) {
- printW("The lyric data is empty!")
- return
- }
- if (this.lyricLines.length <= 0) {
- printW("The lyric lines is empty!")
- return
- }
- let index = this.getIndex(mediaPosition)
- if (index != this.currentIndex) {
- this.animateToIndex(index)
- }
- }
- }
|