主頁 > 移動端開發 > Android原生PDF功能實作

Android原生PDF功能實作

2020-09-16 08:49:05 移動端開發

PDF Demo 效果

1、背景

近期,公司希望實作安卓原生端的PDF功能,要求:高效、實用,

經過兩天的調研、編碼,實作了一個簡單Demo,如上圖所示,
關于安卓原生端的PDF功能實作,技術點還是很多的,為了咱們安卓開發的同學少走彎路,通過此文章,簡單講解下Demo的實作原理和主要技術點,并附上原始碼,

2、安卓PDF現狀

目前,PDF功能仍然是安卓的一個短板,不像iOS,有官方強大的PDF Kit可供集成,
不過,安卓也有一些主流的方案,不過各有優缺點:

1、google doc 在線閱讀,基于webview,國內需翻墻訪問(不可行)
2、跳轉設備中默認pdf app打開,前提需要手機安裝了pdf 軟體(可按需選擇)
3、內置 android-pdfview,基于原生native, apk增加約15~20M(可行,不過安裝包有點大)
4、內置 mupdf,基于原生native, 集成有點麻煩,增加約9M(可行,不過安裝包稍有點大)
5、內置 pdf.js,功能豐富,apk增加5M(基于Webview,性能低,js實作,功能定制復雜)
6、使用x5內核,需要客戶端完全使用x5內核(基于Webview,性能低,不能定制功能)

查閱官方資料,這些方案雖然能實作基本的PDF閱讀功能,但是多數方案,集成程序較復雜,且性能低下,容易記憶體溢位造成App閃退,

3、方案選擇

經過對各方案的反復比對,本次實作PDF Demo,決定使用:android-pdfview,
原因:

1、android-pdfview基于PDFium實作(PDFium是谷歌 + 福昕軟體的PDF開源專案);
2、android-pdfview Github仍在維護;
3、android-pdfview Github獲得的星星較多;
4、客戶端集成較方便;

問題分析:
運行android-pdfview官方demo,問題也很多:

1、僅實作了pdf滑動閱讀、手勢伸縮的功能;
2、缺少pdf目錄樹、縮略圖等功能;
3、安裝包過大;
4、UI不美觀;
5、記憶體問題;
6、其他...

不過,不用擔心,解決了這些問題不就沒有問題了嘛,哈、哈、哈(笑聲有點勉強哈)

下面,咱們開始實作Demo吧,

4、Demo設計

4.1、工程結構

在設計之前,應明確Demo的實作目標:

1、android-pdfview已實作了pdfview,可用于閱讀pdf檔案,手勢伸縮pdf頁面、跳轉pdf頁面,
   那么,咱們基于android-pdfview擴展功能即可,功能包括:目錄樹、縮略圖等;

2、擴展的功能應邏輯解耦,不能影響android-pdfview代碼的可替換性
  (即:如果android-pdfview有新版本,直接替換即可)

3、客戶端應很方便集成
  (如:客戶端僅需要傳遞過來pdf檔案,所有的加載、操作、記憶體管理均無需關心)

Demo工程如何設計:
下載android-pdfview最新原始碼,可以看到共包含兩個Moudle:

android-pdf-viewer(最新原始碼)
sample (示例app)

如果,我們要接管封裝pdf的所有功能,讓sample只傳遞pdf檔案即可,且不影響將來替換android-pdf-viewer的原始碼,那么我們創建一個modle即可,如下圖:

sample (依賴pdfui)
pdfui (依賴android-pdf-viewer)
android-pdf-viewer

4.2、PDF功能設計

為了便于用戶閱讀PDF,應該包含以下功能:
1、PDF閱讀(包含:手指滑動pdf頁面、手勢伸縮頁面內容、跳轉pdf指定頁面)
2、PDF目錄導航功能(包含:目錄展示、目錄節點折疊、展開、點擊跳轉pdf頁面)
3、PDF縮略圖導航功能(包含:縮略圖展示、手指滑動、圖片快取管理、點擊跳轉pdf頁面)

PDF功能代碼結構

5、編碼之前,先解決安裝包過大的問題

反編譯Demo的安裝包,可以看到,安裝包中默認集成了各cpu平臺對應的so庫檔案,安裝包過大的原因也就在這兒,其實正常專案開發中,對于各cpu平臺對應的so庫的保留或舍棄,主要考慮cpu平臺兼容性、設備覆寫率,

通常情況下,僅保留armeabi-v7a可以兼容市面上絕大多數安卓設備,那么,如何編譯時洗掉其他的so呢?

可在android gradle中配置,如下:

android{
......
 splits {
        abi {
            enable true
            reset()
            include 'armeabi-v7a' //如果想包含其他cpu平臺使用的so,修改這里即可
        }
    }
}

重新編譯,生成的安裝包,僅剩5M左右了,

注意:如果專案中還有其他so庫,要根據專案實際需求,認真思考如何取舍了,

6、實作PDF閱讀功能

很簡單,因為android-pdf-viewer原始碼中已經實作了該功能,我們寫一份精簡版的吧,

6.1、功能點:

1、可加載assets中的pdf檔案
2、可加載uri型別的pdf檔案(如果是線上的pdf檔案,可通過網路庫先下載到本地,取其uri,本次Demo就不寫網路下載了)
3、pdf的基本展示功能(使用android-pdf-viewer的控制元件實作:PDFView)
4、可跳轉至目錄頁面(目錄資料可通過intent直接傳遞過去)
5、可跳轉至預覽頁面(pdf檔案資訊可通過intent直接傳遞過去)
6、根據目錄頁面、預覽頁面帶回的頁碼,跳轉至指定的pdf頁面

PDF閱讀功能效果圖

6.2、代碼實作

重點內容:

1、PDFView控制元件的使用;(比較簡單,詳見代碼)
2、如何從PDF檔案中獲得目錄資訊;(如何獲得目錄資訊、什么時機獲取,詳見代碼)

PDF閱讀頁面的代碼:PDFActivity

/**
 * UI頁面:PDF閱讀
 * <p>
 * 主要功能:
 * 1、接收傳遞過來的pdf檔案(包括assets中的檔案名、檔案uri)
 * 2、顯示PDF檔案
 * 3、接收目錄頁面、預覽頁面回傳的PDF頁碼,跳轉到指定的頁面
 * <p>
 * 作者:齊行超
 * 日期:2019.08.07
 */
public class PDFActivity extends AppCompatActivity implements
        OnPageChangeListener,
        onl oadCompleteListener,
        OnPageErrorListener {
    //PDF控制元件
    PDFView pdfView;
    //按鈕控制元件:回傳、目錄、縮略圖
    Button btn_back, btn_catalogue, btn_preview;
    //頁碼
    Integer pageNumber = 0;
    //PDF目錄集合
    List<TreeNodeData> catelogues;

    //pdf檔案名(限:assets里的檔案)
    String assetsFileName;
    //pdf檔案uri
    Uri uri;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        UIUtils.initWindowStyle(getWindow(), getSupportActionBar());//設定沉浸式
        setContentView(R.layout.activity_pdf);

        initView();//初始化view
        setEvent();//設定事件
        loadPdf();//加載PDF檔案
    }

    /**
     * 初始化view
     */
    private void initView() {
        pdfView = findViewById(R.id.pdfView);
        btn_back = findViewById(R.id.btn_back);
        btn_catalogue = findViewById(R.id.btn_catalogue);
        btn_preview = findViewById(R.id.btn_preview);
    }

    /**
     * 設定事件
     */
    private void setEvent() {
        //回傳
        btn_back.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                PDFActivity.this.finish();
            }
        });
        //跳轉目錄頁面
        btn_catalogue.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(PDFActivity.this, PDFCatelogueActivity.class);
                intent.putExtra("catelogues", (Serializable) catelogues);
                PDFActivity.this.startActivityForResult(intent, 200);
            }
        });
        //跳轉縮略圖頁面
        btn_preview.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(PDFActivity.this, PDFPreviewActivity.class);
                intent.putExtra("AssetsPdf", assetsFileName);
                intent.setData(uri);
                PDFActivity.this.startActivityForResult(intent, 201);
            }
        });
    }

    /**
     * 加載PDF檔案
     */
    private void loadPdf() {
        Intent intent = getIntent();
        if (intent != null) {
            assetsFileName = intent.getStringExtra("AssetsPdf");
            if (assetsFileName != null) {
                displayFromAssets(assetsFileName);
            } else {
                uri = intent.getData();
                if (uri != null) {
                    displayFromUri(uri);
                }
            }
        }
    }

    /**
     * 基于assets顯示 PDF 檔案
     *
     * @param fileName 檔案名稱
     */
    private void displayFromAssets(String fileName) {
        pdfView.fromAsset(fileName)
                .defaultPage(pageNumber)
                .onPageChange(this)
                .enableAnnotationRendering(true)
                .onLoad(this)
                .scrollHandle(new DefaultScrollHandle(this))
                .spacing(10) // 單位 dp
                .onPageError(this)
                .pageFitPolicy(FitPolicy.BOTH)
                .load();
    }

    /**
     * 基于uri顯示 PDF 檔案
     *
     * @param uri 檔案路徑
     */
    private void displayFromUri(Uri uri) {
        pdfView.fromUri(uri)
                .defaultPage(pageNumber)
                .onPageChange(this)
                .enableAnnotationRendering(true)
                .onLoad(this)
                .scrollHandle(new DefaultScrollHandle(this))
                .spacing(10) // 單位 dp
                .onPageError(this)
                .load();
    }

    /**
     * 當成功加載PDF:
     * 1、可獲取PDF的目錄資訊
     *
     * @param nbPages the number of pages in this PDF file
     */
    @Override
    public void loadComplete(int nbPages) {
        //獲得檔案書簽資訊
        List<PdfDocument.Bookmark> bookmarks = pdfView.getTableOfContents();
        if (catelogues != null) {
            catelogues.clear();
        } else {
            catelogues = new ArrayList<>();
        }
        //將bookmark轉為目錄資料集合
        bookmarkToCatelogues(catelogues, bookmarks, 1);
    }

    /**
     * 將bookmark轉為目錄資料集合(遞回)
     *
     * @param catelogues 目錄資料集合
     * @param bookmarks  書簽資料
     * @param level      目錄樹級別(用于控制樹節點位置偏移)
     */
    private void bookmarkToCatelogues(List<TreeNodeData> catelogues, List<PdfDocument.Bookmark> bookmarks, int level) {
        for (PdfDocument.Bookmark bookmark : bookmarks) {
            TreeNodeData nodeData = https://www.cnblogs.com/qixingchao/p/new TreeNodeData();
            nodeData.setName(bookmark.getTitle());
            nodeData.setPageNum((int) bookmark.getPageIdx());
            nodeData.setTreeLevel(level);
            nodeData.setExpanded(false);
            catelogues.add(nodeData);
            if (bookmark.getChildren() != null && bookmark.getChildren().size() > 0) {
                List treeNodeDatas = new ArrayList<>();
                nodeData.setSubset(treeNodeDatas);
                bookmarkToCatelogues(treeNodeDatas, bookmark.getChildren(), level + 1);
            }
        }
    }

    @Override
    public void onPageChanged(int page, int pageCount) {
        pageNumber = page;
    }

    @Override
    public void onPageError(int page, Throwable t) {
    }

    /**
     * 從縮略圖、目錄頁面帶回頁碼,跳轉到指定PDF頁面
     *
     * @param requestCode
     * @param resultCode
     * @param data
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK) {
            int pageNum = data.getIntExtra("pageNum", 0);
            if (pageNum > 0) {
                pdfView.jumpTo(pageNum);
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //是否記憶體
        if (pdfView != null) {
            pdfView.recycle();
        }
    }
}

PDF閱讀頁面的布局檔案:activity_pdf.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <RelativeLayout
        android:id="@+id/rl_top"
        android:layout_width="match_parent"
        android:layout_height="70dp"
        android:layout_alignParentTop="true"
        android:background="#03a9f5">

        <Button
            android:id="@+id/btn_back"
            android:layout_width="60dp"
            android:layout_height="30dp"
            android:background="@drawable/shape_button"
            android:text="回傳"
            android:textColor="#ffffff"
            android:textSize="18sp"
            android:layout_alignParentBottom="true"
            android:layout_marginBottom="10dp"
            android:layout_marginLeft="10dp"/>

        <Button
            android:id="@+id/btn_catalogue"
            android:layout_width="60dp"
            android:layout_height="30dp"
            android:background="@drawable/shape_button"
            android:text="目錄"
            android:textColor="#ffffff"
            android:textSize="18sp"
            android:layout_alignParentRight="true"
            android:layout_alignParentBottom="true"
            android:layout_marginBottom="10dp"
            android:layout_marginRight="10dp"/>

        <Button
            android:id="@+id/btn_preview"
            android:layout_width="60dp"
            android:layout_height="30dp"
            android:background="@drawable/shape_button"
            android:text="預覽"
            android:textColor="#ffffff"
            android:textSize="18sp"
            android:layout_toLeftOf="@+id/btn_catalogue"
            android:layout_alignParentBottom="true"
            android:layout_marginBottom="10dp"
            android:layout_marginRight="10dp"/>
    </RelativeLayout>

    <com.github.barteksc.pdfviewer.PDFView
        android:id="@+id/pdfView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/rl_top"/>

</RelativeLayout>

7、PDF目錄樹的實作

目錄樹的資料(目錄名稱、頁碼...),已在上個頁面獲取了,所以此頁面只需考慮目錄樹控制元件的實作,

注意:之所以沒在這個頁面單獨獲取目錄樹的資料,主要考慮到android-pdfview、pdfium記憶體占用太大了,不想再次創建Pdf的相關物件,

7.1、PDF目錄樹效果圖

7.2、樹形控制元件如何實作?

安卓默認沒有樹形控制元件,不過我們可以使用RecyclerView或ListView實作,
如上圖所示:

串列每一行為一條目錄資料,主要包括:名稱、頁碼;
如果有子目錄,則出現箭頭圖片,該項可折疊、展開,箭頭方向隨之改變;
子目錄的名稱文本隨目錄樹級別遞增向右偏移;

當前Demo實作方式為RecyclerView,應該如何實作上面的效果?
可在adapter中處理頁面效果、事件效果:
1、串列項內容展示

1、使用垂直線性布局管理器;
2、每個item包含:箭頭圖片(如果有子目錄,則顯示)、命令名稱文本、頁碼文本;

2、折疊效果

1、控制adapter資料集合的內容即可,如果某節點折疊了,就把對應的子目錄資料洗掉即可,
反之,加上,再notifyDataSetChanged通知資料源改變;
2、除此之外,還需有一個狀態來標記當前節點是展開還是折疊,用于控制箭頭圖片方向的顯示;

3、目錄文本向右偏移效果

可通過目錄樹層級 * 固定左側間隔(如: 20dp),然后為目錄的textview控制元件設定偏移即可;

目錄樹層級樹如何獲取? 可選方案:
1、遞回集合自動獲取(需要遍歷,效率低一點,如果是可編輯的目錄結構,建議選擇)
2、創建資料的時候,直接寫死(因當前demo的PDF目錄結構不會被編輯,所以直接選擇這個方案吧)

7.3、代碼實作:

樹形控制元件的資料物件TreeNodeData:

/**
 * 樹形控制元件資料類(會用于頁面間傳輸,所以需實作Serializable 或 Parcelable)
 * 作者:齊行超
 * 日期:2019.08.07
 */
public class TreeNodeData implements Serializable {
    //名稱
    private String name;
    //頁碼
    private int pageNum;
    //是否已展開(用于控制樹形節點圖片顯示,即箭頭朝向圖片)
    private boolean isExpanded;
    //展示級別(1級、2級...,用于控制樹形節點縮進位置)
    private int treeLevel;
    //子集(用于加載子節點,也用于判斷是否顯示箭頭圖片,如集合不為空,則顯示)
    private List<TreeNodeData> subset;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPageNum() {
        return pageNum;
    }

    public void setPageNum(int pageNum) {
        this.pageNum = pageNum;
    }

    public boolean isExpanded() {
        return isExpanded;
    }

    public void setExpanded(boolean expanded) {
        isExpanded = expanded;
    }

    public int getTreeLevel() {
        return treeLevel;
    }

    public void setTreeLevel(int treeLevel) {
        this.treeLevel = treeLevel;
    }

    public List<TreeNodeData> getSubset() {
        return subset;
    }

    public void setSubset(List<TreeNodeData> subset) {
        this.subset = subset;
    }
}

樹形控制元件配接器 : TreeAdapter

/**
 * 樹形控制元件配接器
 * 作者:齊行超
 * 日期:2019.08.07
 */
public class TreeAdapter extends RecyclerView.Adapter<TreeAdapter.TreeNodeViewHolder> {
    //背景關系
    private Context context;
    //資料
    public List<TreeNodeData> data;
    //展示資料(由層級結構改為平面結構)
    public List<TreeNodeData> displayData;
    //treelevel間隔(dp)
    private int maginLeft;
    //委托物件
    private TreeEvent delegate;

    /**
     * 建構式
     *
     * @param context 背景關系
     * @param data    資料
     */
    public TreeAdapter(Context context, List<TreeNodeData> data) {
        this.context = context;
        this.data = https://www.cnblogs.com/qixingchao/p/data;
        maginLeft = UIUtils.dip2px(context, 20);
        displayData = new ArrayList<>();

        //資料轉為展示資料
        dataToDiaplayData(data);
    }

    /**
     * 資料轉為展示資料
     *
     * @param data 資料
     */
    private void dataToDiaplayData(List data) {
        for (TreeNodeData nodeData : data) {
            displayData.add(nodeData);
            if (nodeData.isExpanded() && nodeData.getSubset() != null) {
                dataToDiaplayData(nodeData.getSubset());
            }
        }
    }

    /**
     * 資料集合轉為可顯示的集合
     */
    private void reDataToDiaplayData() {
        if (this.data == null || this.data.size() == 0) {
            return;
        }
        if(displayData == null){
            displayData = new ArrayList<>();
        }else{
            displayData.clear();
        }
        dataToDiaplayData(this.data);
        notifyDataSetChanged();
    }

    @Override
    public TreeNodeViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(context).inflate(R.layout.tree_item, null);
        return new TreeNodeViewHolder(view);
    }

    @Override
    public void onBindViewHolder(TreeNodeViewHolder holder, int position) {
        final TreeNodeData data = displayData.get(position);
        //設定圖片
        if (data.getSubset() != null) {
            holder.img.setVisibility(View.VISIBLE);
            if (data.isExpanded()) {
                holder.img.setImageResource(R.drawable.arrow_h);
            } else {
                holder.img.setImageResource(R.drawable.arrow_v);
            }
        } else {
            holder.img.setVisibility(View.INVISIBLE);
        }
        //設定圖片偏移位置
        RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) holder.img.getLayoutParams();
        int ratio = data.getTreeLevel() <= 0? 0 : data.getTreeLevel()-1;
        params.setMargins(maginLeft * ratio, 0, 0, 0);
        holder.img.setLayoutParams(params);

        //顯示文本
        holder.title.setText(data.getName());
        holder.pageNum.setText(String.valueOf(data.getPageNum()));

        //圖片點擊事件
        holder.img.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //控制樹節點展開、折疊
                data.setExpanded(!data.isExpanded());
                //重繪資料源
                reDataToDiaplayData();
            }
        });
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //回呼結果
                if(delegate!=null){
                    delegate.onSelectTreeNode(data);
                }
            }
        });
    }

    @Override
    public int getItemCount() {
        return displayData.size();
    }

    /**
     * 定義RecyclerView的ViewHolder物件
     */
    class TreeNodeViewHolder extends RecyclerView.ViewHolder {
        ImageView img;
        TextView title;
        TextView pageNum;

        public TreeNodeViewHolder(View view) {
            super(view);
            img = view.findViewById(R.id.iv_arrow);
            title = view.findViewById(R.id.tv_title);
            pageNum = view.findViewById(R.id.tv_pagenum);
        }
    }

    /**
     * 介面:Tree事件
     */
    public interface TreeEvent{
        /**
         * 當選擇了某tree節點
         * @param data tree節點資料
         */
        void onSelectTreeNode(TreeNodeData data);
    }

    /**
     * 設定Tree的事件
     * @param treeEvent Tree的事件物件
     */
    public void setTreeEvent(TreeEvent treeEvent){
        this.delegate = treeEvent;
    }
}

PDF目錄樹頁面:PDFCatelogueActivity

/**
 * UI頁面:PDF目錄
 * <p>
 * 1、用于顯示Pdf目錄資訊
 * 2、點擊tree item,帶回Pdf頁碼到前一個頁面
 * <p>
 * 作者:齊行超
 * 日期:2019.08.07
 */
public class PDFCatelogueActivity extends AppCompatActivity implements TreeAdapter.TreeEvent {

    RecyclerView recyclerView;
    Button btn_back;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        UIUtils.initWindowStyle(getWindow(), getSupportActionBar());
        setContentView(R.layout.activity_catelogue);

        initView();//初始化控制元件
        setEvent();//設定事件
        loadData();//加載資料
    }

    /**
     * 初始化控制元件
     */
    private void initView() {
        btn_back = findViewById(R.id.btn_back);
        recyclerView = findViewById(R.id.rv_tree);
    }

    /**
     * 設定事件
     */
    private void setEvent() {
        btn_back.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                PDFCatelogueActivity.this.finish();
            }
        });
    }

    /**
     * 加載資料
     */
    private void loadData() {
        //從intent中獲得傳遞的資料
        Intent intent = getIntent();
        List<TreeNodeData> catelogues = (List<TreeNodeData>) intent.getSerializableExtra("catelogues");

        //使用RecyclerView加載資料
        LinearLayoutManager llm = new LinearLayoutManager(this);
        llm.setOrientation(LinearLayoutManager.VERTICAL);
        recyclerView.setLayoutManager(llm);
        TreeAdapter adapter = new TreeAdapter(this, catelogues);
        adapter.setTreeEvent(this);
        recyclerView.setAdapter(adapter);
    }


    /**
     * 點擊tree item,帶回Pdf頁碼到前一個頁面
     *
     * @param data tree節點資料
     */
    @Override
    public void onSelectTreeNode(TreeNodeData data) {
        Intent intent = new Intent();
        intent.putExtra("pageNum", data.getPageNum());
        setResult(Activity.RESULT_OK, intent);
        finish();
    }
}

PDF目錄樹的布局檔案:activity_catelogue.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <RelativeLayout
        android:id="@+id/rl_top"
        android:layout_width="match_parent"
        android:layout_height="70dp"
        android:layout_alignParentTop="true"
        android:background="#03a9f5">

        <Button
            android:id="@+id/btn_back"
            android:layout_width="60dp"
            android:layout_height="30dp"
            android:layout_alignParentBottom="true"
            android:layout_marginLeft="10dp"
            android:layout_marginBottom="10dp"
            android:background="@drawable/shape_button"
            android:text="回傳"
            android:textColor="#ffffff"
            android:textSize="18sp" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_centerHorizontal="true"
            android:layout_marginBottom="15dp"
            android:text="目錄串列"
            android:textColor="#ffffff"
            android:textSize="18sp" />
    </RelativeLayout>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv_tree"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/rl_top" />

</RelativeLayout>

8、PDF預覽縮略圖

這個功能算是本Demo中最為復雜的一個了:

如何將PDF某頁面的內容轉成圖片?(默認是無法從pdfview中獲得頁面圖片的)
如何減少圖片記憶體的占用?(用戶可能快速滑動串列,實時讀取、顯示多張圖片)
如何優化PDF預覽縮略圖串列的滑動體驗?(圖片的獲取需要一定時間)
如何合理的及時釋放記憶體占用?

8.1、PDF預覽縮略圖串列的效果圖

8.2、功能分析

1、如何將PDF某頁面的內容轉成圖片?

查看android-pdfview的原始碼,無法通過PDFView控制元件獲得某頁面的圖片,所以只能分析pdfium sdk的API了,如下圖:

pdfium的renderPageBitmap方法可以將頁面渲染成圖片,不過需要傳遞一系列引數,而且要小心OutOfMemoryError,

那么,我們需要在代碼中獲取或者創建PdfiumCore物件,呼叫該方法,傳遞PdfDocument等引數,當bitmap使用完后,應及時釋放掉,

2、如何減少記憶體的占用?

記憶體主要包括:
1、pdfium sdk加載pdf檔案產生的記憶體(我們無法優化)
2、android-pdfview產生的記憶體(如果有需要,可改其原始碼)
3、我們將pdf頁面轉為縮略圖,而產生的記憶體(必須優化,否則,容易oom)

3.1、當PdfiumCore、PdfDocument不再使用時,應及時關閉;
3.2、當縮略圖不再使用時,應及時釋放;
3.3、可使用LruCache臨時快取縮略圖,防止重復呼叫renderPageBitmap獲取圖片;
3.4、LruCache應合理管控,當預覽頁面關閉時,必須清空快取,以釋放記憶體;
3.5、創建圖片時,應使用RGB_565,能節約記憶體開銷(一個像素點,占2位元組)
3.6、創建圖片時,應盡可能小的指定圖片的寬高,能看清就行(圖片占用的記憶體 = 寬 * 高 * 一個像素點占的位元組數)

3、如何優化PDF預覽縮略圖串列的滑動體驗?

查看pdfium原始碼,呼叫renderPageBitmap方法之前,還必須確保對應的頁面已被打開,即呼叫了openPage方法,然而,這兩個方法都需要一定時間才能執行完成的,

那么,如果我們直接在主執行緒中讓每個RecylerVew的item分別調用renderPageBitmap方法,滑動串列時,會感覺特別卡,所以該方法只能放在子執行緒中呼叫了,

那么問題又來了,那么多子執行緒應該如何管控?

1、考慮CPU的占用,應使用執行緒池控制子執行緒并發、阻塞;
2、考慮到用戶滑動速度,有可能某執行緒正執行或者阻塞著呢,頁面已經滑過去了,那么,即使該執行緒加載出來了圖片,也無法顯示到串列中,所以對于RecyclerView已不可見的Item項對應的執行緒,應及時取消,防止做無用功,也節省了記憶體和cpu開銷,

8.3、功能實作

預覽縮略圖工具類:PreviewUtils

/**
 * 預覽縮略圖工具類
 *
 * 1、pdf頁面轉為縮略圖
 * 2、圖片快取管理(僅保存到記憶體,可使用LruCache,注意空間大小控制)
 * 3、多執行緒管理(執行緒并發、阻塞、Future任務取消)
 *
 * 作者:齊行超
 * 日期:2019.08.08
 */
public class PreviewUtils {
    //圖片快取管理
    private ImageCache imageCache;
    //單例
    private static PreviewUtils instance;
    //執行緒池
    ExecutorService executorService;
    //執行緒任務集合(可用于取消任務)
    HashMap<String, Future> tasks;

    /**
     * 單例(僅主執行緒呼叫,無需做成執行緒安全的)
     *
     * @return PreviewUtils實體物件
     */
    public static PreviewUtils getInstance() {
        if (instance == null) {
            instance = new PreviewUtils();
        }
        return instance;
    }

    /**
     * 默認建構式
     */
    private PreviewUtils() {
        //初始化圖片快取管理物件
        imageCache = new ImageCache();
        //創建并發執行緒池(建議最大并發數大于1屏grid item的數量)
        executorService = Executors.newFixedThreadPool(20);
        //創建執行緒任務集合,用于取消執行緒執行
        tasks = new HashMap<>();
    }

    /**
     * 從pdf檔案中加載圖片
     *
     * @param context     背景關系
     * @param imageView   圖片控制元件
     * @param pdfiumCore  pdf核心物件
     * @param pdfDocument pdf檔案物件
     * @param pdfName     pdf檔案名稱
     * @param pageNum     pdf頁碼
     */
    public void loadBitmapFromPdf(final Context context,
                                  final ImageView imageView,
                                  final PdfiumCore pdfiumCore,
                                  final PdfDocument pdfDocument,
                                  final String pdfName,
                                  final int pageNum) {
        //判斷引數合法性
        if (imageView == null || pdfiumCore == null || pdfDocument == null || pageNum < 0) {
            return;
        }

        try {
            //快取key
            final String keyPage = pdfName + pageNum;

            //為圖片控制元件設定標記
            imageView.setTag(keyPage);

            Log.i("PreViewUtils", "加載pdf縮略圖:" + keyPage);

            //獲得imageview的尺寸(注意:如果使用正常控制元件尺寸,太占記憶體了)
            /*int w = imageView.getMeasuredWidth();
            int h = imageView.getMeasuredHeight();
            final int reqWidth = w == 0 ? UIUtils.dip2px(context,100) : w;
            final int reqHeight = h == 0 ? UIUtils.dip2px(context,150) : h;*/

            //記憶體大小= 圖片寬度 * 圖片高度 * 一個像素占的位元組數(RGB_565 所占位元組:2)
            //注意:如果使用正常控制元件尺寸,太占記憶體了,所以此處指定四縮略圖看著會模糊一點
            final int reqWidth = 100;
            final int reqHeight = 150;

            //從快取中取圖片
            Bitmap bitmap = imageCache.getBitmapFromLruCache(keyPage);
            if (bitmap != null) {
                imageView.setImageBitmap(bitmap);
                return;
            }

            //使用執行緒池管理子執行緒
            Future future = executorService.submit(new Runnable() {
                @Override
                public void run() {
                    //打開頁面(呼叫renderPageBitmap方法之前,必須確保頁面已open,重要)
                    pdfiumCore.openPage(pdfDocument, pageNum);

                    //呼叫native方法,將Pdf頁面渲染成圖片
                    final Bitmap bm = Bitmap.createBitmap(reqWidth, reqHeight, Bitmap.Config.RGB_565);
                    pdfiumCore.renderPageBitmap(pdfDocument, bm, pageNum, 0, 0, reqWidth, reqHeight);

                    //切回主執行緒,設定圖片
                    if (bm != null) {
                        //將圖片加入快取
                        imageCache.addBitmapToLruCache(keyPage, bm);

                        //切回主執行緒加載圖片
                        new Handler(Looper.getMainLooper()).post(new Runnable() {
                            @Override
                            public void run() {
                                if (imageView.getTag().toString().equals(keyPage)) {
                                    imageView.setImageBitmap(bm);
                                    Log.i("PreViewUtils", "加載pdf縮略圖:" + keyPage + "......已設定!!");
                                }
                            }
                        });
                    }
                }
            });

            //將任務添加到集合
            tasks.put(keyPage, future);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    /**
     * 取消從pdf檔案中加載圖片的任務
     *
     * @param keyPage 頁碼
     */
    public void cancelLoadBitmapFromPdf(String keyPage) {
        if (keyPage == null || !tasks.containsKey(keyPage)) {
            return;
        }
        try {
            Log.i("PreViewUtils", "取消加載pdf縮略圖:" + keyPage);
            Future future = tasks.get(keyPage);
            if (future != null) {
                future.cancel(true);
                Log.i("PreViewUtils", "取消加載pdf縮略圖:" + keyPage + "......已取消!!");
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    /**
     * 獲得圖片快取物件
     * @return 圖片快取
     */
    public ImageCache getImageCache(){
        return imageCache;
    }

    /**
     * 圖片快取管理
     */
   public class ImageCache {
        //圖片快取
        private LruCache<String, Bitmap> lruCache;

        //建構式
        public ImageCache() {
            //初始化 lruCache
            //int maxMemory = (int) Runtime.getRuntime().maxMemory();
            //int cacheSize = maxMemory/8;
            int cacheSize = 1024 * 1024 * 30;//暫時設定30M
            lruCache = new LruCache<String, Bitmap>(cacheSize) {
                @Override
                protected int sizeOf(String key, Bitmap value) {
                    return value.getRowBytes() * value.getHeight();
                }
            };
        }

        /**
         * 從快取中取圖片
         * @param key 鍵
         * @return 圖片
         */
        public synchronized Bitmap getBitmapFromLruCache(String key) {
            if(lruCache!= null) {
                return lruCache.get(key);
            }
            return null;
        }

        /**
         * 向快取中加圖片
         * @param key 鍵
         * @param bitmap 圖片
         */
        public synchronized void addBitmapToLruCache(String key, Bitmap bitmap) {
            if (getBitmapFromLruCache(key) == null) {
                if (lruCache!= null && bitmap != null)
                    lruCache.put(key, bitmap);
            }
        }

        /**
         * 清空快取
         */
        public void clearCache(){
            if(lruCache!= null){
                lruCache.evictAll();
            }
        }
    }
}

grid串列配接器: GridAdapter

/**
 * grid串列配接器
 * 作者:齊行超
 * 日期:2019.08.08
 */
public class GridAdapter extends RecyclerView.Adapter<GridAdapter.GridViewHolder> {

    Context context;
    PdfiumCore pdfiumCore;
    PdfDocument pdfDocument;
    String pdfName;
    int totalPageNum;


    public GridAdapter(Context context, PdfiumCore pdfiumCore, PdfDocument pdfDocument, String pdfName, int totalPageNum) {
        this.context = context;
        this.pdfiumCore = pdfiumCore;
        this.pdfDocument = pdfDocument;
        this.pdfName = pdfName;
        this.totalPageNum = totalPageNum;
    }

    @Override
    public GridViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(context).inflate(R.layout.grid_item, null);
        return new GridViewHolder(view);
    }

    @Override
    public void onBindViewHolder(GridViewHolder holder, int position) {
        //設定PDF圖片
        final int pageNum = position;
        PreviewUtils.getInstance().loadBitmapFromPdf(context, holder.iv_page, pdfiumCore, pdfDocument, pdfName, pageNum);
        //設定PDF頁碼
        holder.tv_pagenum.setText(String.valueOf(position));
        //設定Grid事件
        holder.iv_page.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(delegate!=null){
                    delegate.onGridItemClick(pageNum);
                }
            }
        });
        return;
    }

    @Override
    public void onViewDetachedFromWindow(GridViewHolder holder) {
        super.onViewDetachedFromWindow(holder);
        try {
            //item不可見時,取消任務
            if(holder.iv_page!=null){
                PreviewUtils.getInstance().cancelLoadBitmapFromPdf(holder.iv_page.getTag().toString());
            }

            //item不可見時,釋放bitmap  (注意:本Demo使用了LruCache快取來管理圖片,此處可注釋掉)
            /*Drawable drawable = holder.iv_page.getDrawable();
            if (drawable != null) {
                Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
                if (bitmap != null && !bitmap.isRecycled()) {
                    bitmap.recycle();
                    bitmap = null;
                    Log.i("PreViewUtils","銷毀pdf縮略圖:"+holder.iv_page.getTag().toString());
                }
            }*/
        }catch (Exception ex){
            ex.printStackTrace();
        }
    }

    @Override
    public int getItemCount() {
        return totalPageNum;
    }

    class GridViewHolder extends RecyclerView.ViewHolder {
        ImageView iv_page;
        TextView tv_pagenum;

        public GridViewHolder(View itemView) {
            super(itemView);
            iv_page = itemView.findViewById(R.id.iv_page);
            tv_pagenum = itemView.findViewById(R.id.tv_pagenum);
        }
    }

    /**
     * 介面:Grid事件
     */
    public interface GridEvent{
        /**
         * 當選擇了某Grid項
         * @param position tree節點資料
         */
        void onGridItemClick(int position);
    }

    /**
     * 設定Grid事件
     * @param event Grid事件物件
     */
    public void setGridEvent(GridEvent event){
        this.delegate = event;
    }

    //Grid事件委托
    private GridEvent delegate;
}

PDF預覽縮略圖頁面:PDFPreviewActivity

/**
 * UI頁面:PDF預覽縮略圖(注意:此頁面,需多關注記憶體管控)
 * <p>
 * 1、用于顯示Pdf縮略圖資訊
 * 2、點擊縮略圖,帶回Pdf頁碼到前一個頁面
 * <p>
 * 作者:齊行超
 * 日期:2019.08.07
 */
public class PDFPreviewActivity extends AppCompatActivity implements GridAdapter.GridEvent {

    RecyclerView recyclerView;
    Button btn_back;
    PdfiumCore pdfiumCore;
    PdfDocument pdfDocument;
    String assetsFileName;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        UIUtils.initWindowStyle(getWindow(), getSupportActionBar());
        setContentView(R.layout.activity_preview);

        initView();//初始化控制元件
        setEvent();
        loadData();
    }

    /**
     * 初始化控制元件
     */
    private void initView() {
        btn_back = findViewById(R.id.btn_back);
        recyclerView = findViewById(R.id.rv_grid);
    }

    /**
     * 設定事件
     */
    private void setEvent() {
        btn_back.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //回收記憶體
                recycleMemory();

                PDFPreviewActivity.this.finish();
            }
        });

    }

    /**
     * 加載資料
     */
    private void loadData() {
        //加載pdf檔案
        loadPdfFile();

        //獲得pdf總頁數
        int totalCount = pdfiumCore.getPageCount(pdfDocument);

        //系結串列資料
        GridAdapter adapter = new GridAdapter(this, pdfiumCore, pdfDocument, assetsFileName, totalCount);
        adapter.setGridEvent(this);
        recyclerView.setLayoutManager(new GridLayoutManager(this, 3));
        recyclerView.setAdapter(adapter);
    }

    /**
     * 加載pdf檔案
     */
    private void loadPdfFile() {
        Intent intent = getIntent();
        if (intent != null) {
            assetsFileName = intent.getStringExtra("AssetsPdf");
            if (assetsFileName != null) {
                loadAssetsPdfFile(assetsFileName);
            } else {
                Uri uri = intent.getData();
                if (uri != null) {
                    loadUriPdfFile(uri);
                }
            }
        }
    }

    /**
     * 加載assets中的pdf檔案
     */
    void loadAssetsPdfFile(String assetsFileName) {
        try {
            File f = FileUtils.fileFromAsset(this, assetsFileName);
            ParcelFileDescriptor pfd = ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY);
            pdfiumCore = new PdfiumCore(this);
            pdfDocument = pdfiumCore.newDocument(pfd);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    /**
     * 基于uri加載pdf檔案
     */
    void loadUriPdfFile(Uri uri) {
        try {
            ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r");
            pdfiumCore = new PdfiumCore(this);
            pdfDocument = pdfiumCore.newDocument(pfd);
        }catch (Exception ex){
            ex.printStackTrace();
        }
    }

    /**
     * 點擊縮略圖,帶回Pdf頁碼到前一個頁面
     *
     * @param position 頁碼
     */
    @Override
    public void onGridItemClick(int position) {
        //回收記憶體
        recycleMemory();

        //回傳前一個頁碼
        Intent intent = new Intent();
        intent.putExtra("pageNum", position);
        setResult(Activity.RESULT_OK, intent);
        finish();
    }

    /**
     * 回收記憶體
     */
    private void recycleMemory(){
        //關閉pdf物件
        if (pdfiumCore != null && pdfDocument != null) {
            pdfiumCore.closeDocument(pdfDocument);
            pdfiumCore = null;
        }
        //清空圖片快取,釋放記憶體空間
        PreviewUtils.getInstance().getImageCache().clearCache();
    }
}

PDF預覽縮略圖頁面的布局檔案:activity_preview.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <RelativeLayout
        android:id="@+id/rl_top"
        android:layout_width="match_parent"
        android:layout_height="70dp"
        android:layout_alignParentTop="true"
        android:background="#03a9f5">

        <Button
            android:id="@+id/btn_back"
            android:layout_width="60dp"
            android:layout_height="30dp"
            android:layout_alignParentBottom="true"
            android:layout_marginLeft="10dp"
            android:layout_marginBottom="10dp"
            android:background="@drawable/shape_button"
            android:text="回傳"
            android:textColor="#ffffff"
            android:textSize="18sp" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_alignParentBottom="true"
            android:layout_centerHorizontal="true"
            android:layout_marginBottom="15dp"
            android:text="預覽縮略圖串列"
            android:textColor="#ffffff"
            android:textSize="18sp" />
    </RelativeLayout>

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv_grid"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_below="@+id/rl_top" />
</RelativeLayout>

總結

檔案中涉及的功能點較多,難點也較多,尤其是記憶體管理、多執行緒管理,有不明白的建議下載Demo,多看下原始碼,也歡迎留言咨詢,就是不一定有時間解答,哈哈,,,,

如果希望把該demo用到專案中,建議多測驗一下,因為時間關系,我這邊僅做了基本測驗,

Demo下載地址(github + 百度網盤):
https://github.com/qxcwanxss/AndroidPdfViewerDemo
https://pan.baidu.com/s/1_Py36avgQqcJ5C87BaS5Iw

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/55638.html

標籤:Android

上一篇:Android自定義控制元件:自適應大小的文本控制元件

下一篇:Flutter學習筆記(28)--使用第三方jar包

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more