鸿蒙JavaUI:PageSlider组件,实现左右无限循环轮播图
一、要点:
1. PageSlider: 提供左右滑动功能。注:目前该组件暂不支持内容循环,该功能需要手动实现。
2. PageSliderProvider: 为PageSlider组件提供页面适配器。
二、实现无限循环的思路
在代码层面,无限循环实际是伪循环。即,尾部拼接一个第一个内容组件,首部拼接一个最后一个内容组件。滑动到首部/尾部时,立刻切换到相应位置。由于速度为非常快,肉眼无法看出,才在应用层面感觉是无限循环。
三、功能拆解
假设:我们有三个图片需要轮播展示,按照顺序应是如下配置:
1. 手指向右滑动,内容向右滚动,实现“左”方向的无限循环
第一步: 在轮播图内容左侧,手动拼接一个“轮播图3”。这样,再继续向右滑动时,给人一种“已经”循环的感觉。注:如果继续滑动,内容时不会动的。
第二步: 使用PageSlider.PageChangedListener监听,获取当前内容的坐标。当坐标为0时,说明已经滚动到了拼接的“轮播图3_拼接”,此时需要立刻切换到“轮播图3”对应的坐标。至此,在理论上,“左”方向的无限循环已经实现。
2. 手指向左滑动动,内容向左滚动,实现“右”方向的无限循环
第一步:在“左”方向无限循环内容拼接的基础上,在右侧手动拼接一个“轮播图1”。这样,向左滑动到“轮播图3”时,再继续滑动,滑动到“轮播图1_拼接”。第二步: 使用PageSlider.PageChangedListener监听,获取当前内容的坐标。当坐标为内容Size - 1时,说明已经滚动到了拼接的“轮播图1_拼接”,此时需要立刻切换到“轮播图1”对应的坐标。至此,在理论上,双向的无限循环已经实现。
3. 单方向自动轮播
自动轮播需要在手指滑动轮播图时,停止轮播,避免与手指滑动发生冲突。当停止手指离开屏幕时,再启动自动轮播。
关键点:
1. 使用定时任务,定时切换PageSlider内容。定时任务需要延迟启动,否则刚加载完成就切换PageSlider内容,有悖常理。
2. 借助Component.TouchEventListener - 触摸监听,该监听可以提供手指按下、多指按下、最后一根手指离开屏幕(关键点)等信息提示。当第一根手指触摸屏幕时,需要关闭定时任务。当最后一根手指离开屏幕时,开启定时任务。实现手动和自动轮播的切换。
四、代码实现
1. xml中添加PageSlider组件
<!-- 顶部轮播图 -->
<PageSlider
ohos:id="$+id:shoot_ability_page_slider"
ohos:height="300vp"
ohos:width="match_content"
/>
2. 继承PageSliderProvider类
PageSliderProvider的createPageInContainer方法,在PageSlider初始化时调用。因此,需要重写该方法,将我们的组件添加到参数componentContainer中。
import ohos.agp.components.Component;
import ohos.agp.components.ComponentContainer;
import ohos.agp.components.Image;
import ohos.agp.components.PageSliderProvider;
import java.util.List;
/**
* Image组件适配器
*
* @author 殷冬
* @date 2021-02-19
* @version 1.0.0
*/
public class BannerProvider extends PageSliderProvider {
private List<Image> listData;
public BannerProvider(List<Image> listData) {
this.listData = listData;
}
@Override
public int getCount() {
return listData.size();
}
@Override
public Object createPageInContainer(ComponentContainer componentContainer, int i) {
Object obj = listData.get(i);
componentContainer.addComponent((Component) obj);
return obj;
}
@Override
public void destroyPageFromContainer(ComponentContainer componentContainer, int i, Object o) {
componentContainer.removeComponent(listData.get(i));
}
@Override
public boolean isPageMatchToObject(Component component, Object o) {
return component == o;
}
}
3. 组装PageSliderProvider继承类数据 - 轮播图内容
这里需要组装内容前后的拼接内容。
/**
* 添加数据
*
* @param imageId 图片资源Id
*/
public ShootMainTopBanner setImages(Integer ... imageId){
List<Image> data = new ArrayList<>();
for(Integer id : imageId){
Image image = new Image(abilitySlice.getContext());
// 设置image的宽高,并且图片充满整个image
image.setLayoutConfig(new ComponentContainer.LayoutConfig(ScreenPixelsUtil.getDisplayWidthInPx(abilitySlice.getContext()), ComponentContainer.LayoutConfig.MATCH_PARENT));
image.setScaleMode(Image.ScaleMode.STRETCH);
image.setPixelMap(id);
data.add(image);
}
// 组装数据,格式 3 1 2 3 1, 实现自动单向滚动和手动双向滚动逻辑,中间123是展示数据,左右3 1 为实现连续滚动的效果数据
images.add(data.get(data.size() - 1));
images.addAll(data);
images.add(data.get(0));
this.dataSize = images.size();
return this;
}
4. 配置PageSlider组件属性
这里需要初始化PageSlider组件的一些属性。
有两个关键点:
1. PageSlider.setCurrentPage(index, true) : 在初始化时调用该方法,组件加载完毕会直接显示指定index的内容,不会有滚动动画。当手动调用时,第二个参数为true,会根据设置的切换时间进行滚动。当第二个参数为false时,会立刻切换到指定的index,不会有滚动动画,这里也是实现首尾接续的关键点。
2. addPageChangedListener: 添加PageSlider组件内容切换监听。
/**
* 初始化轮播图
*/
public void start(){
globalTaskDispatcher = abilitySlice.getGlobalTaskDispatcher(TaskPriority.DEFAULT);
BannerProvider bannerProvider = new BannerProvider(images);
pageSlider = (PageSlider) abilitySlice.findComponentById(ResourceTable.Id_shoot_ability_page_slider);
pageSlider.setProvider(bannerProvider);
// 设置初始页面平滑滚动
pageSlider.setCurrentPage(index, true);
// 设置滚动方向
pageSlider.setOrientation(Component.HORIZONTAL);
// 设置切换时间,值越大切换速度越慢
pageSlider.setPageSwitchTime(pageSwitchTime);
// 启用页面滑动
pageSlider.setSlidingPossible(true);
// 添加监听
pageSlider.addPageChangedListener(new ChangedListener());
// 启动自动切换
this.startPlaying();
// 添加监听
this.addTouchEventListener(pageSlider);
}
5. PageChangedListener回调中,实现手指无限循环滑动
在onPageChosen回调中,实现切换逻辑。
关键点:
1. 立即切换: setCurrentPage(index, false); false false false....;
1. 由于自动切换中,调用setCurrentPage方法,也会触发回调。需要添加一个boolean类型变量,区分是手动滑动还是自动轮播;
2. 这里不需要判断滑动方向,通过index的极限值[0, size - 1],来切换到对应的内容。
① i = 0: 说明已经滑动到最左侧,需要立刻切换到最后一个内容,即index = size - 1 - 1;注:size - 1: 最后面拼接的第一个内容“轮播图1_拼接”。 size - 1 - 1: 真实的最后一个内容;
② i = size - 1: 说明已经滑动到最右侧,需要立刻切换到第一个内容,即index = 1;注: 0的位置,是拼接的最后一个内容“轮播图3_拼接”。
/**
* 切换监听
*/
class ChangedListener implements PageSlider.PageChangedListener{
/**
* 选择新页面的回调
*
* @param i 指示所显示页的位置索引。
* @param v 指示页的位置偏移量。值范围是(0,1]。0表示显示相同的页面; 1表示显示目标页面。
* @param i1 指示所显示页面的位置偏移像素数。
*/
@Override
public void onPageSliding(int i, float v, int i1) {
HiLog.info(LOG, "--------------------onPageSliding: i: %{public}s; v: %{public}s; i1: %{public}s;", i, v, i1);
}
/**
* 页面幻灯片状态更改时回调
* @param i 指示页面状态。该值可以是0、1或2,分别表示页处于空闲状态、拖动状态或滑动状态。
*/
@Override
public void onPageSlideStateChanged(int i) {
HiLog.info(LOG, "--------------------onPageSlideStateChanged: i: %{public}s", i);
}
/**
* 页面滑动时回调
*
* @param i 指示所选页的索引。
*/
@Override
public void onPageChosen(int i) {
HiLog.info(LOG, "--------------------onPageChosen: i: %{public}s", i);
// 更新index
index = i;
// 处理手动滑动逻辑
if(! isAutoSlide){
// 向左滑动,轮播图向右滚动
if(index == dataSize - 1){
index = 1;
pageSlider.setCurrentPage(index, false);
}
// 向右滑动,轮播图向左滚动
if(index == 0){
index = images.size() - 2;
pageSlider.setCurrentPage(index, false);
}
}
}
}
6. 定时任务实现自动轮播
使用Executors.newSingleThreadScheduledExecutor();延迟线程池,延迟启动轮播任务,避免第一次加载就切换轮播图内容。
关键点:
1. Componet.TouchEventListener(): 通过触摸事件,当有手指触摸组件时,停止轮播任务。当最后一根手指离开屏幕时,重新创建轮播任务,继续自动轮播;
2. 动画切换:setCurrentPage(index, true);这里需要有切换动画,true;
/**
* 定时器,自动切换图片
*/
private void startPlaying(){
// 初始化线程池
this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
// 设置线程池
this.scheduledExecutorService.scheduleWithFixedDelay(automaticSwitching, TIME_INTERVAL, TIME_INTERVAL, TimeUnit.MILLISECONDS);
}
/**
* 停止轮播释放资源
*/
public void stopPlaying() {
scheduledExecutorService.shutdown();
}
/**
* 轮播图触摸事件
*
* @param pageSlider
*/
public void addTouchEventListener(PageSlider pageSlider){
pageSlider.setTouchEventListener(new Component.TouchEventListener() {
@Override
public boolean onTouchEvent(Component component, TouchEvent touchEvent) {
int action = touchEvent.getAction();
// 最后一根手指抬起、取消、没有触摸活动时, 启动定时器
if(action == PRIMARY_POINT_UP || action == CANCEL || action == NONE) {
isAutoSlide = true;
startPlaying();
} else if (action == PRIMARY_POINT_DOWN) {
isAutoSlide = false;
// 存在触摸时,停止自动切换
stopPlaying();
}
return true;
}
});
}
/**
* 切换轮播图内容
*/
class AutomaticSwitching implements Runnable{
@Override
public void run() {
abilitySlice.getUITaskDispatcher().asyncDispatch(() -> {
index++;
pageSlider.setCurrentPage(index, true);
if(index == dataSize - 1){
// 切换到第一个位置, 且立即切换
globalTaskDispatcher.asyncDispatch(new SlideShowTaskLast());
}
});
}
}
class SlideShowTaskLast implements Runnable{
@Override
public void run() {
try{
Thread.sleep(pageSwitchTime);
}catch (Exception e){
}
abilitySlice.getUITaskDispatcher().syncDispatch(() -> {
index = 1;
pageSlider.setCurrentPage(index, false);
});
}
}
7. 完整的代码
package com.yindong.splitlens.pageability.shoot.main;
import com.yindong.common.enums.HiLogEnum;
import com.yindong.common.utils.ScreenPixelsUtil;
import com.yindong.splitlens.ResourceTable;
import com.yindong.splitlens.pageability.shoot.main.components.BannerProvider;
import ohos.aafwk.ability.AbilitySlice;
import ohos.agp.components.Component;
import ohos.agp.components.ComponentContainer;
import ohos.agp.components.Image;
import ohos.agp.components.PageSlider;
import ohos.app.dispatcher.TaskDispatcher;
import ohos.app.dispatcher.task.TaskPriority;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import ohos.multimodalinput.event.TouchEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import static ohos.multimodalinput.event.TouchEvent.*;
import static ohos.multimodalinput.event.TouchEvent.PRIMARY_POINT_DOWN;
/**
* 顶部轮播图
*
* @author 殷冬
* @date 2021-02-19
* @version 1.0.0
*/
public class ShootMainTopBanner {
private static final HiLogLabel LOG = new HiLogLabel(HiLog.LOG_APP, HiLogEnum.LOG_DOMAIN.getCode(), ShootMainTopBanner.class.getName());
private AbilitySlice abilitySlice;
/**
* 处理轮播图收尾链接线程
*/
private TaskDispatcher globalTaskDispatcher;
/**
* 线程池
*/
private ScheduledExecutorService scheduledExecutorService;
/**
* 默认展示第一个, 跳过第一个虚拟展示数据
*/
private Integer index = 1;
/**
* 自动切换时间
*/
private static final Integer TIME_INTERVAL = 5000;
/**
* 切换过程时间,单位毫秒
*/
private Integer pageSwitchTime = 500;
/**
* 轮播数据长度
*/
private Integer dataSize;
/**
* 轮播图片内容
*/
private List<Image> images = new ArrayList<>();
private PageSlider pageSlider;
/**
* 是否处于自动轮播状态
*/
private boolean isAutoSlide = true;
/**
* 自动切换任务
*/
private AutomaticSwitching automaticSwitching = new AutomaticSwitching();
public ShootMainTopBanner(AbilitySlice abilitySlice){
this.abilitySlice = abilitySlice;
}
/**
* 添加数据
*
* @param imageId 图片资源Id
*/
public ShootMainTopBanner setImages(Integer ... imageId){
List<Image> data = new ArrayList<>();
for(Integer id : imageId){
Image image = new Image(abilitySlice.getContext());
// 设置image的宽高,并且图片充满整个image
image.setLayoutConfig(new ComponentContainer.LayoutConfig(ScreenPixelsUtil.getDisplayWidthInPx(abilitySlice.getContext()), ComponentContainer.LayoutConfig.MATCH_PARENT));
image.setScaleMode(Image.ScaleMode.STRETCH);
image.setPixelMap(id);
data.add(image);
}
// 组装数据,格式 3 1 2 3 1, 实现自动单向滚动和手动双向滚动逻辑,中间123是展示数据,左右3 1 为实现连续滚动的效果数据
images.add(data.get(data.size() - 1));
images.addAll(data);
images.add(data.get(0));
this.dataSize = images.size();
return this;
}
/**
* 初始化轮播图
*/
public void start(){
globalTaskDispatcher = abilitySlice.getGlobalTaskDispatcher(TaskPriority.DEFAULT);
BannerProvider bannerProvider = new BannerProvider(images);
pageSlider = (PageSlider) abilitySlice.findComponentById(ResourceTable.Id_shoot_ability_page_slider);
pageSlider.setProvider(bannerProvider);
// 设置初始页面平滑滚动
pageSlider.setCurrentPage(index, true);
// 设置滚动方向
pageSlider.setOrientation(Component.HORIZONTAL);
// 设置切换时间,值越大切换速度越慢
pageSlider.setPageSwitchTime(pageSwitchTime);
// 启用页面滑动
pageSlider.setSlidingPossible(true);
// 添加监听
pageSlider.addPageChangedListener(new ChangedListener());
// 启动自动切换
this.startPlaying();
// 添加监听
this.addTouchEventListener(pageSlider);
}
/**
* 定时器,自动切换图片
*/
private void startPlaying(){
// 初始化线程池
this.scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
// 设置线程池
this.scheduledExecutorService.scheduleWithFixedDelay(automaticSwitching, TIME_INTERVAL, TIME_INTERVAL, TimeUnit.MILLISECONDS);
}
/**
* 停止轮播释放资源
*/
public void stopPlaying() {
scheduledExecutorService.shutdown();
}
/**
* 轮播图触摸事件
*
* @param pageSlider
*/
public void addTouchEventListener(PageSlider pageSlider){
pageSlider.setTouchEventListener(new Component.TouchEventListener() {
@Override
public boolean onTouchEvent(Component component, TouchEvent touchEvent) {
int action = touchEvent.getAction();
// 最后一根手指抬起、取消、没有触摸活动时, 启动定时器
if(action == PRIMARY_POINT_UP || action == CANCEL || action == NONE) {
isAutoSlide = true;
startPlaying();
} else if (action == PRIMARY_POINT_DOWN) {
isAutoSlide = false;
// 存在触摸时,停止自动切换
stopPlaying();
}
return true;
}
});
}
/**
* 切换轮播图内容
*/
class AutomaticSwitching implements Runnable{
@Override
public void run() {
abilitySlice.getUITaskDispatcher().asyncDispatch(() -> {
index++;
pageSlider.setCurrentPage(index, true);
if(index == dataSize - 1){
// 切换到第一个位置, 且立即切换
globalTaskDispatcher.asyncDispatch(new SlideShowTaskLast());
}
});
}
}
class SlideShowTaskLast implements Runnable{
@Override
public void run() {
try{
Thread.sleep(pageSwitchTime);
}catch (Exception e){
}
abilitySlice.getUITaskDispatcher().syncDispatch(() -> {
index = 1;
pageSlider.setCurrentPage(index, false);
});
}
}
/**
* 切换监听
*/
class ChangedListener implements PageSlider.PageChangedListener{
/**
* 选择新页面的回调
*
* @param i 指示所显示页的位置索引。
* @param v 指示页的位置偏移量。值范围是(0,1]。0表示显示相同的页面; 1表示显示目标页面。
* @param i1 指示所显示页面的位置偏移像素数。
*/
@Override
public void onPageSliding(int i, float v, int i1) {
HiLog.info(LOG, "--------------------onPageSliding: i: %{public}s; v: %{public}s; i1: %{public}s;", i, v, i1);
}
/**
* 页面幻灯片状态更改时回调
* @param i 指示页面状态。该值可以是0、1或2,分别表示页处于空闲状态、拖动状态或滑动状态。
*/
@Override
public void onPageSlideStateChanged(int i) {
HiLog.info(LOG, "--------------------onPageSlideStateChanged: i: %{public}s", i);
}
/**
* 页面滑动时回调
*
* @param i 指示所选页的索引。
*/
@Override
public void onPageChosen(int i) {
HiLog.info(LOG, "--------------------onPageChosen: i: %{public}s", i);
// 更新index
index = i;
// 处理手动滑动逻辑
if(! isAutoSlide){
// 向左滑动,轮播图向右滚动
if(index == dataSize - 1){
index = 1;
pageSlider.setCurrentPage(index, false);
}
// 向右滑动,轮播图向左滚动
if(index == 0){
index = images.size() - 2;
pageSlider.setCurrentPage(index, false);
}
}
}
}
}
8. 调用方式
// 初始化轮播图
ShootMainTopBanner shootMainTopBanner = new ShootMainTopBanner(abilitySlice);
shootMainTopBanner.setImages(ResourceTable.Media_shoot_main_banner_1,
ResourceTable.Media_shoot_main_banner_2,
ResourceTable.Media_shoot_main_banner_3)
.start();
9. 工具类
/**
* 获取屏幕真实宽度
*
* @param context 上下文
* @return
*/
public static int getDisplayWidthInPx(Context context) {
Display display = DisplayManager.getInstance().getDefaultDisplay(context).get();
Point point = new Point();
display.getRealSize(point);
return (int) point.getPointX();
}