鸿蒙JavaUI:PageSlider组件,实现左右无限循环轮播图

deanyuancn
发布于 2021-5-7 18:32
浏览
0收藏

一、要点:


1. PageSlider: 提供左右滑动功能。注:目前该组件暂不支持内容循环,该功能需要手动实现。
2. PageSliderProvider: 为PageSlider组件提供页面适配器。


二、实现无限循环的思路
在代码层面,无限循环实际是伪循环。即,尾部拼接一个第一个内容组件,首部拼接一个最后一个内容组件。滑动到首部/尾部时,立刻切换到相应位置。由于速度为非常快,肉眼无法看出,才在应用层面感觉是无限循环。


三、功能拆解
假设:我们有三个图片需要轮播展示,按照顺序应是如下配置:鸿蒙JavaUI:PageSlider组件,实现左右无限循环轮播图 -鸿蒙开发者社区

1.  手指向右滑动,内容向右滚动,实现“左”方向的无限循环

第一步: 在轮播图内容左侧,手动拼接一个“轮播图3”。这样,再继续向右滑动时,给人一种“已经”循环的感觉。注:如果继续滑动,内容时不会动的。

鸿蒙JavaUI:PageSlider组件,实现左右无限循环轮播图 -鸿蒙开发者社区

第二步: 使用PageSlider.PageChangedListener监听,获取当前内容的坐标。当坐标为0时,说明已经滚动到了拼接的“轮播图3_拼接”,此时需要立刻切换到“轮播图3”对应的坐标。至此,在理论上,“左”方向的无限循环已经实现。鸿蒙JavaUI:PageSlider组件,实现左右无限循环轮播图 -鸿蒙开发者社区

2. 手指向左滑动动,内容向左滚动,实现“右”方向的无限循环

第一步:在“左”方向无限循环内容拼接的基础上,在右侧手动拼接一个“轮播图1”。这样,向左滑动到“轮播图3”时,再继续滑动,滑动到“轮播图1_拼接”。鸿蒙JavaUI:PageSlider组件,实现左右无限循环轮播图 -鸿蒙开发者社区第二步: 使用PageSlider.PageChangedListener监听,获取当前内容的坐标。当坐标为内容Size - 1时,说明已经滚动到了拼接的“轮播图1_拼接”,此时需要立刻切换到“轮播图1”对应的坐标。至此,在理论上,双向的无限循环已经实现。鸿蒙JavaUI:PageSlider组件,实现左右无限循环轮播图 -鸿蒙开发者社区

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();
}

 

已于2021-5-7 18:32:44修改
收藏
回复
举报
回复
    相关推荐