节切换效果的原理及实现

Posted zzy0516alex

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了节切换效果的原理及实现相关的知识,希望对你有一定的参考价值。

文章目录

android ViewPager2 实现阅读器横向翻页效果(三)— 实时动态分页及章节切换效果的原理及实现

本系列最终效果图如下:

本文实现的效果如下:

关键概念引入

第二章中,我们引入了Java Bean NovelContentPage,它可以表示文本分页之后一个页面的文字内容,也可用通过isTempPage属性表示一个完整的(临时的)章节,如下图所示:

那么这些NovelContentPage,还需要一个类来存放和管理它们,这里我们就使用一个窗的概念(Window of Pages)来封装我们的页面:
Class NovelPageWindow

public class NovelPageWindow 
    private ArrayList<NovelContentPage> pages;
    private boolean isSingleWindow = true;//表示当前Window仅装载一个章节的内容
    private int chapID;//仅single window有效

    public ArrayList<NovelContentPage> getPages() 
        return pages;
    

    public void setPages(ArrayList<NovelContentPage> pages) 
        this.pages = pages;
    

    public int getPageNum()
        return pages!=null?pages.size():1;
    

    public boolean isSingleWindow() 
        return isSingleWindow;
    

    public void setSingleWindow(boolean singleWindow) 
        isSingleWindow = singleWindow;
    

    public int getChapID() 
        return chapID;
    

    public void setChapID(int chapID) 
        this.chapID = chapID;
    


从意义上来说,NovelPageWindow存放的是当前ViewPager将要展示的页面内容,根据实际可能的情形,Window可分为如下几种:

为表征当前页面相对于缓冲区(临时页)的位置,映入枚举类型WindowType

private enum WindowTypeleft,right,match

其中,left和right主要是为了在用户翻到本章节的首页或尾页时,向ViewPager追加内容,以实现跨章节翻页的无缝衔接。
为了对window中的页面有清晰的索引,我们需要引入两个索引变量:

private int window_page_index;//页面在整个window中的索引
private int chap_page_index;//页面在自身章节中的索引

其对应关系如下图所示:

下面,我将以一组章节为例,按照动态分页的逻辑顺序来进行讲解:

初始数据准备

String content = "第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n"
                +"第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n"
                +"第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n"
                +"第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n"
                +"第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n"
                +"第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n"
                +"第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n"
                +"第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n"
                +"第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n"
                +"第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n"
                +"第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n"
                +"第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n"
                +"第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n第一章第一章第一章第一章第一章第一章第一章\\n";

        String content2 = "第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n"
                +"第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n"
                +"第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n"
                +"第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n"
                +"第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n"
                +"第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n"
                +"第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n"
                +"第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n"
                +"第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n"
                +"第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n第二章第二章第二章第二章第二章第二章第二章\\n";


        String content3 = "第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n"
                +"第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n"
                +"第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n"
                +"第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n"
                +"第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n"
                +"第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n"
                +"第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n"
                +"第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n"
                +"第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n"
                +"第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n第三章第三章第三章第三章第三章第三章第三章\\n";

        NovelContentPage full_content1 = new NovelContentPage();
        full_content1.setPage_id(0);full_content1.setPage_content(content);full_content1.setTempPage(true);full_content1.setBelong_to_chapID(0);
        NovelContentPage full_content2 = new NovelContentPage();
        full_content2.setPage_id(1);full_content2.setPage_content(content2);full_content2.setTempPage(true);full_content2.setBelong_to_chapID(1);
        NovelContentPage full_content3 = new NovelContentPage();
        full_content3.setPage_id(2);full_content3.setPage_content(content3);full_content3.setTempPage(true);full_content3.setBelong_to_chapID(2);

        ArrayList<NovelContentPage> pages;

        NovelPageWindow chap1 = new NovelPageWindow();
        pages = new ArrayList<>();pages.add(full_content1);
        chap1.setPages(pages);
        NovelPageWindow chap2 = new NovelPageWindow();
        pages = new ArrayList<>();pages.add(full_content2);
        chap2.setPages(pages);
        NovelPageWindow chap3 = new NovelPageWindow();
        pages = new ArrayList<>();pages.add(full_content3);
        chap3.setPages(pages);
        chapList.add(chap1);
        chapList.add(chap2);
        chapList.add(chap3);

准备三个章节的文字内容,将它们分别以临时页的形式装入NovelContentPage中,再将这三个临时Page装入三个window中,最后用一个ArrayList chapList来存放所有的三个章节。
接下来进行章节初始化:
选定上述三个章节中的其中一章作为起始章节,显然当前的windowType是Match,当前的window是single window:

chap_index = 0;
chap_page_index = 0
window_page_index = chap_page_index;
currentNovelChap = chapList.get(chap_index);
windowType = WindowType.match;
currentNovelChap.setSingleWindow(true);

当前的窗口如下图所示

ViewPager Adapter 动态分页 及 第一次分页

为了保证分页的准确性和灵活性,并且能适应动态的字体调整,这里选择本系列第二章所讲的Attach后分页的方式。这就要求预先绘制TextView再进行分页。所以我们采用在adapter中先用临时页进行绘制,在进行分页后覆盖的方式:

public class PageAdapter extends RecyclerView.Adapter<PageAdapter.PageViewHolder> 

    private ArrayList<NovelContentPage> pages;
    private Context context;
    private NovelPageWindow chap;
    private PageListener pageListener;

    public PageAdapter(Context context, NovelPageWindow novelPageWindow)
        this.chap = novelPageWindow;
        this.context = context;
    

    public void setPageListener(PageListener pageListener) 
        this.pageListener = pageListener;
    

    @NonNull
    @Override
    public PageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) 
        PageViewHolder pageViewHolder = new PageViewHolder(LayoutInflater.from(context).inflate(R.layout.page_layout, parent, false));
        return pageViewHolder;
    

    @Override
    public void onBindViewHolder(@NonNull PageViewHolder holder, int position) 
        pages = chap.getPages();
        NovelContentPage novelContentPage = pages.get(position);
        holder.tv_content.setText(novelContentPage.getPage_content());
        holder.tv_id.setText(String.valueOf(novelContentPage.getPage_id()));
        if (novelContentPage.isTempPage()) 
            Log.d("bind view","found temp page: "+position);
            holder.tv_content.post(()->
                ArrayList<NovelContentPage> page = PageSplit.getPage(novelContentPage.getPage_content(), holder.tv_content);
                if (chap.isSingleWindow())pages = page;
                else 
                    int i = pages.indexOf(novelContentPage);
                    pages.addAll(i,page);
                    pages.remove(novelContentPage);
                
                chap.setPages(pages);
                if (pageListener!=null)pageListener.onPageSplitDone(chap,page,novelContentPage.getBelong_to_chapID());
            );
        

    

    @Override
    public int getItemCount() 
        return chap.getPageNum();
    

    class PageViewHolder extends RecyclerView.ViewHolder 
        public RelativeLayout mContainer;
        public TextView tv_id;
        public TextView tv_content;

        public PageViewHolder(@NonNull View itemView) 
            super(itemView);
            tv_id = itemView.findViewById(R.id.page_id);
            tv_content = itemView.findViewById(R.id.page_content);
            mContainer = itemView.findViewById(R.id.card_container);
        
    

    public interface PageListener
        void onPageSplitDone(NovelPageWindow update_chap, ArrayList<NovelContentPage> new_pages, int chapID);
    

其中的重点在onBindViewHolder,对tv_content设置好临时的长章节文本后,利用post方法,在TextView绘制后对其进行分页。并且将分页的结果利用PageListener接口进行回调。
分页后此时的窗口如下图所示:

分页后更新窗口 及 首页尾页的特殊处理

分页后,分页的信息由PageListener接口回调,要更新窗口则需要实现onPageSplitDone方法,如下:

pageListener = (update_chap, pages1, chapID) -> 
            chapList.get(chapID).setPages(pages1);
            //根据chap_page_index 调整window_page_index
            int total_chap = currentNovelChap.getPageNum();
            int total_window = pageView.getAdapter().getItemCount();
            switch(windowType)
                case left:
                    NovelPageWindow left_chap = chapList.get(chap_index - 1);
                    window_page_index = chap_page_index + left_chap.getPageNum();
                
                break;
                case right:
                    window_page_index = chap_page_index;
                
                break;
                case match:
                    window_page_index = chap_page_index;
                    if (chap_page_index!=0 && init_page)init_page = false;
                
                    break;
                default:
            
            pageView.post(()->
                PageAdapter adapter = new PageAdapter(context,update_chap);
                adapter.setName("split update");
                adapter.setPageListener(pageListener);
                pageView.setAdapter(adapter);
                pageView.setCurrentItem(window_page_index,false);
            );

            Log.d("page split",String.format("chap %d/%d",chap_page_index, total_chap -1));
            Log.d("page split",String.format("window %d/%d",window_page_index, total_window -1));

分页后首先要根据当前页相对于新分页内容的位置,更新窗口页面索引window_page_index,这里在初次分页时,窗口类型为match无需对索引进行更改。接下来需要更新PageView以应用分页结果,注意这里一定不能用notifyDataSetChanged()方法进行更新,这样会导致部分页面未加载而产生空白页,必须新建一个adapter对原来的进行覆盖,该操作需要在原布局绘制完成后再进行。
下面是使用notifyDataSetChanged()方法和新建adapter覆盖的效果比较:

如果当前页正好是一章的开头或者结尾,则需要提前将上一章/下一章的临时页添加到窗口中:

pageListener = (update_chap, pages1, chapID) -> 
            //窗口更新部分同上,省略
            ...
            //首页尾页的特殊处理
            if (chap_page_index == 0 && window_page_index == 0)
                Log.d("page split","start from the first page of a chap");
                if(chap_index > 0)
                    ArrayList<NovelContentPage> temp_pages = new ArrayList<>(chapList.get(chap_index - 1).getPages());
                    window_page_index += temp_pages.size();
                    temp_pages.addAll(currentNovelChap.getPages());
                    NovelPageWindow novelPageWindow = new NovelPageWindow();
                    novelPageWindow.setPages(temp_pages);
                    novelPageWindow.setSingleWindow(false);
                    after_update = true;
                    PageAdapter adapter = new PageAdapter(context, novelPageWindow);
                    adapter.setPageListener(pageListener);
                    adapter.setName("split addon");
                    pageView.post(()->
                        pageView.setAdapter(adapter);
                    );
                    if (init_page)init_page = false;
                    windowType = WindowType.left;
                
                else 
                    Log.d("page split","start from the first page of the novel");
                    windowType = WindowType.match;
                    if (init_page)init_page = false;
                
            
            if (chap_page_index == (total_chap-1) && window_page_index == (total_window-1))
                Log.android界面切换增加了animation特效,但是没有实现动画效果(已经执行到setAni

Android移动应用开发之使用异步调用进度条及实现幻灯片切换效果

[Android] 任意时刻从子线程切换到主线程的实现原理及加强版

Android零基础入门第70节:ViewPager轻松完成TabHost效果

Android实现ViewPager视差动画效果及背景渐变过渡

Android安卓下拉阻尼效果实现原理及简单实例