縦方向に連続スクロールするニュースティッカーは、情報量が多く表示領域に制約がある場面で特に有効なUIパターンです。モバイルアプリやダッシュボード、デジタルサイネージなどで、限られた空間に動的に情報を流すことで、ユーザーの視線を引きつけつつ多くのコンテンツを伝達できます。以下では、このコンポーネントの設計思想から実装、自動生成までを解説します。
一、動作原理と設計方針
本コンポーネントは、表示領域の外にデータをループ配置し、margin-topを逐次更新することでスクロール動作を実現します。データの末尾と先頭をシームレスに接続することで、無限ループのような視覚効果を生み出します。
主要な設計ポイントは以下の通りです:
- 表示行数(
visibleCount)とデータ総数を比較し、少ない方を基準に初期化 - データ配列の末尾に、先頭から
visibleCount個分の要素を複製 - ホバー・タッチ時は
setIntervalをクリアし、離れた際に再開 - 行ごとの移動は
requestAnimationFrameではなく、細分化されたsetIntervalで段階的に実行
二、ユースケース
| 場面 | 具体例 |
|---|---|
| ニュースアプリ | 速報タイトルの巡回表示 |
| IoTダッシュボード | 異常検知ログのリアルタイム反映 |
| 店舗向けアプリ | キャンペーン情報の自動案内 |
| 社内ポータル | お知らせ欄のコンパクト化 |
三、コンポーネント実装
以下に、再設計したコンポーネントの完全なコードを示します。変数名・構造を刷新しつつ、元の動作を維持しています。
<template>
<view
class="vertical-ticker"
:style="{ height: rowPx * displayRows + 'rpx' }"
@touchstart="haltScroll"
@touchend="resumeScroll"
@mouseover="haltScroll"
@mouseleave="resumeScroll"
>
<view
class="ticker-track"
:style="{ transform: `translateY(-${offsetY}rpx)` }"
>
<slot
v-for="(entry, idx) in cyclicBuffer"
:key="idx"
:record="entry"
:rowPx="rowPx"
></slot>
</view>
</view>
</template>
<script>
export default {
name: 'VerticalNewsTicker',
props: {
source: { type: Array, default: () => [] },
visibleRows: { type: Number, default: 4 },
rowPx: { type: Number, default: 80 },
transitionMs: { type: Number, default: 500 },
intervalMs: { type: Number, default: 2000 }
},
data() {
return {
cyclicBuffer: [],
offsetY: 0,
actualRows: 0,
cycleTimer: null,
frameTimer: null
}
},
computed: {
maxScroll() {
return this.source.length * this.rowPx;
}
},
mounted() {
this.bootstrap();
},
beforeDestroy() {
this.clearAllTimers();
},
methods: {
bootstrap() {
this.clearAllTimers();
this.actualRows = Math.min(this.visibleRows, this.source.length);
this.cyclicBuffer = [...this.source, ...this.source.slice(0, this.actualRows)];
this.offsetY = 0;
this.cycleTimer = setInterval(() => this.shiftByOne(), this.intervalMs);
},
shiftByOne() {
if (this.offsetY >= this.maxScroll) {
this.offsetY = 0;
}
const stepCount = this.rowPx;
const stepDelay = this.transitionMs / this.rowPx;
let progressed = 0;
this.frameTimer = setInterval(() => {
this.offsetY += 1;
progressed++;
if (progressed >= stepCount) {
clearInterval(this.frameTimer);
this.frameTimer = null;
}
}, stepDelay);
},
haltScroll() {
this.clearAllTimers();
},
resumeScroll() {
this.bootstrap();
},
clearAllTimers() {
if (this.cycleTimer) {
clearInterval(this.cycleTimer);
this.cycleTimer = null;
}
if (this.frameTimer) {
clearInterval(this.frameTimer);
this.frameTimer = null;
}
}
},
watch: {
source: {
deep: true,
handler() {
this.bootstrap();
}
}
}
}
</script>
<style scoped>
.vertical-ticker {
width: 100%;
overflow: hidden;
position: relative;
}
.ticker-track {
will-change: transform;
}
</style>
四、呼び出し側の実装例
以下は、APIから取得したデータをバインングし、カスタムレイアウトで表示する実装例です。スタイル変数を整理し、可読性を向上させています。
<template>
<view class="page-root">
<view class="ticker-wrapper">
<vertical-news-ticker
v-slot="{ record }"
:source="feedItems"
:visibleRows="3"
:rowPx="236"
:transitionMs="500"
:intervalMs="2000"
>
<view class="news-card" :style="{ height: '236rpx' }">
<view class="news-body">
<view class="headline-area">
<text class="headline-text">{{ record.title }}</text>
</view>
<view class="summary-area">
<text class="summary-text">{{ record.remark }}</text>
</view>
</view>
</view>
</vertical-news-ticker>
</view>
</view>
</template>
<script>
export default {
data() {
return {
feedItems: [],
apiEndpoint: 'https://php.diygw.com/article.php'
}
},
onLoad() {
this.loadFeed();
},
methods: {
async loadFeed() {
try {
const response = await this.$http.post(this.apiEndpoint, {}, {}, 'json');
this.feedItems = response.data || [];
} catch (err) {
console.error('フィード取得失敗:', err);
}
}
}
}
</script>
<style lang="scss" scoped>
.ticker-wrapper {
height: 708rpx;
}
.news-card {
padding: 20rpx;
margin: 20rpx;
border-radius: 12rpx;
background: #fff;
box-shadow: 0 0 28rpx 11px rgba(218, 255, 225, 0.84),
0 0 10rpx 1px rgba(31, 31, 31, 0.06);
}
.headline-area {
margin: 10rpx;
}
.headline-text {
font-weight: 700;
font-size: 32rpx;
}
.summary-area {
margin: 10rpx;
}
.summary-text {
color: #34b39d;
font-size: 28rpx;
}
</style>
五、低コードプラットフォームでの自動生成対応
本コンポーネントは、ビジュアルエディタ上で以下の属性を設定可能とすることで、コード生成器との連携を容易にしています。
| 設定項目 | 型 | 説明 |
|---|---|---|
| データソース | Array/Object | バインドする配列またはAPI応答 |
| 可視行数 | Number | 同時に表示する行数 |
| 行高さ | Number | 各行の高さ(rpx) |
| 移動時間 | Number | 1行分のスクロールにかける時間(ms) |
| 待機間隔 | Number | 次のスクロール開始までの間隔(ms) |
| スロットテンプレート | VNode | 各行のレイアウト定義 |
コード生成器は、上記設定を解析し、対応するprops値を埋め込んだコンポーネントタグを出力します。スロット部分は、FLEXレイアウトエディタで構築したツリーをそのまま子要素として展開することで、デザインとロジックの分離を実現します。
六、パフォーマンスと注意点
本実装において留意すべき事項は以下の通りです:
- タイマー管理: コンポーネント破棄時に必ず全ての
setIntervalをクリアし、メモリリークを防止する - タッチ・マウスの競合: モバイルとデスクトップ双方のイベントをリッスンし、重複発火を制御する
- データ更新時のリセット:
sourceプロパティ変更時はオフセットを0に戻し、表示の不整合を防ぐ - CSS Transformの活用:
margin-topではなくtranslateYを使用し、レイアウト再計算コストを削減する(上記コードではrpx対応のため数値管理としているが、実際の運用ではpx換算後のtransform推奨)