diff --git a/doric-demo/src/ScrolledSliderDemo.ts b/doric-demo/src/ScrolledSliderDemo.ts index 65f2345d..68252e82 100644 --- a/doric-demo/src/ScrolledSliderDemo.ts +++ b/doric-demo/src/ScrolledSliderDemo.ts @@ -1,4 +1,4 @@ -import { Group, text, gravity, Color, LayoutSpec, vlayout, hlayout, layoutConfig, scroller, Text, ViewHolder, VMPanel, ViewModel, network, loge, HLayout, stack, image, Gravity, takeNonNull, Scroller, Image } from "doric"; +import { Group, text, gravity, Color, LayoutSpec, vlayout, hlayout, layoutConfig, scroller, Text, ViewHolder, VMPanel, ViewModel, network, loge, HLayout, stack, image, Gravity, takeNonNull, Scroller, Image, View } from "doric"; import { colors } from "./utils"; import MovieData from './movie.json' @@ -67,15 +67,20 @@ interface DoubanModel { interface MovieModel { doubanModel?: DoubanModel selectedIdx: number + anchorPos: number } +const frameWidth = 200 +const frameHeight = 300 +const padding = 20 + class MovieVH extends ViewHolder { title!: Text gallery!: HLayout scrolled!: Scroller movieTitle!: Text movieYear!: Text - + anchor!: View build(root: Group) { vlayout( [ @@ -110,9 +115,20 @@ class MovieVH extends ViewHolder { }), vlayout( [ - text({ - text: "↑", - }), + stack( + [ + this.anchor = image({ + imageBase64: "data:image/png;base64,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", + layoutConfig: layoutConfig().just(), + width: 30, + height: 30, + }), + ], + { + layoutConfig: layoutConfig().fit().configWidth(LayoutSpec.MOST), + } + ), + this.movieTitle = text({ textSize: 20, }), @@ -140,51 +156,95 @@ class MovieVM extends ViewModel{ network(context).get("https://douban.uieee.com/v2/movie/top250").then(ret => { this.updateState(state => state.doubanModel = JSON.parse(ret.data) as DoubanModel) }) + this.updateState(state => { + state.anchorPos = padding + frameWidth / 2 + state.selectedIdx = 0 + }) + let scrollX = 0 vh.scrolled.onScroll = (offset) => { - const frameWidth = 270 / 2 * 1.5 - const centerX = offset.x + Environment.screenWidth / 2 - const idx = Math.floor(centerX / frameWidth) - if (state.selectedIdx != idx) { + if (offset.x < 0 || offset.x > (state.doubanModel?.count || 0) * frameWidth + padding * 2 - Environment.screenWidth) { + return + } + const dx = offset.x - scrollX + scrollX = offset.x + const idx = Math.floor((offset.x + state.anchorPos - padding) / frameWidth) + if (state.selectedIdx !== idx) { this.updateState(state => state.selectedIdx = idx) } - const overOff = Math.abs(centerX - frameWidth * (idx + 0.5)) - const scale = overOff / (frameWidth / 2) - takeNonNull(this.images.get(idx))(v => { - v.scaleX = v.scaleY = 1.5 - 0.5 * scale + takeNonNull(this.images.get(idx))(it => { + const scale = (offset.x + state.anchorPos - (idx + 0.5) * frameWidth - padding) / (frameWidth / 2) + it.scaleX = it.scaleY = 1.5 - Math.abs(scale * 0.5) }) + this.updateArrow() } } + updateArrow() { + takeNonNull(this.images.get(this.getState().selectedIdx))(it => { + it.getLocationOnScreen(context).then(ret => { + this.getViewHolder().anchor.centerX = ret.x + frameWidth / 2 + }) + }) + } + + onItemClicked(idx: number) { + takeNonNull(this.images.get(this.getState().selectedIdx)?.superview)(it => { + it.scaleX = it.scaleY = 1 + }) + takeNonNull(this.images.get(idx)?.superview)(it => { + it.getLocationOnScreen(context).then(ret => { + let anchor = this.getState().anchorPos + if (ret.x < 0) { + this.getViewHolder().scrolled.scrollBy(context, { x: ret.x, y: 0 }, true) + anchor = frameWidth / 2 + } else if (ret.x > Environment.screenWidth - frameWidth) { + this.getViewHolder().scrolled.scrollBy( + context, + { x: ret.x - (Environment.screenWidth - frameWidth), y: 0 }, + true) + anchor = Environment.screenWidth - frameWidth / 2 + } else { + anchor = ret.x + frameWidth / 2 + } + this.updateState(state => { + state.selectedIdx = idx + state.anchorPos = anchor + }) + }) + }) + } + onBind(state: MovieModel, vh: MovieVH) { if (state.doubanModel) { vh.title.text = state.doubanModel.title vh.gallery.children.length = 0 + const vm = this state.doubanModel.subjects.forEach((e, idx) => { vh.gallery.addChild(stack( [ image({ layoutConfig: layoutConfig().just().configAlignment(Gravity.Center), - width: 270 / 2, - height: 400 / 2, + width: frameWidth / 1.5, + height: frameHeight / 1.5, imageUrl: e.images.large, onClick: function () { const v = (this as Image).superview if (v == undefined) { return } - v.getLocationOnScreen(context).then(ret => { - const centerX = ret.x + v.width / 2; - vh.scrolled.scrollBy(context, { x: centerX - Environment.screenWidth / 2, y: 0 }, true) - }) + vm.onItemClicked(idx) }, }).also(it => { this.images.set(idx, it) + if (state.selectedIdx == idx) { + it.scaleX = it.scaleY = 1.5 + } }) ], { layoutConfig: layoutConfig().just(), - width: 270 / 2 * 1.5, - height: 400 / 2 * 1.5, + width: frameWidth, + height: frameHeight, })) }) takeNonNull(state.doubanModel.subjects[state.selectedIdx])(it => { @@ -192,6 +252,8 @@ class MovieVM extends ViewModel{ vh.movieYear.text = it.year }) } + vh.anchor.centerX = state.anchorPos + this.updateArrow() } } @@ -205,7 +267,7 @@ class SliderPanel extends VMPanel{ } getState() { - return { selectedIdx: 0, doubanModel: MovieData } + return { selectedIdx: 0, anchorPos: Environment.screenWidth / 2, doubanModel: MovieData } } } \ No newline at end of file