Android 如何從零開(kāi)始寫(xiě)一款書(shū)籍閱讀器的示例
一款書(shū)籍閱讀器,需要以下功能才能說(shuō)的上比較完整:
- 文字頁(yè)面展示,即書(shū)頁(yè);
- 頁(yè)面之間的跳轉(zhuǎn)動(dòng)畫(huà),即翻頁(yè)動(dòng)作;
- 能夠在每一頁(yè)上記錄閱讀進(jìn)度,即書(shū)簽;
- 能夠自由選擇文字并標(biāo)注,即筆記;
- 能夠設(shè)置一些屬性,如屏幕亮度,字體大小,主體顏色等,即個(gè)性化設(shè)置。

書(shū)籍閱讀器
這篇文章帶來(lái)的就是如何打造這么一款閱讀器。(由于整體代碼量比較大,所以我只能說(shuō)說(shuō)我的實(shí)現(xiàn)思路再加上部分的核心代碼來(lái)說(shuō)明,不會(huì)有太多的代碼展示。)
翻頁(yè)動(dòng)作——搭建整個(gè)閱讀器的框架
在閱讀器上的翻頁(yè)動(dòng)作無(wú)外乎仿真和平移這兩種動(dòng)畫(huà),翻頁(yè)時(shí)需要準(zhǔn)備兩張頁(yè)面,一張是當(dāng)前頁(yè),另一張是需要翻轉(zhuǎn)的下一頁(yè)。翻頁(yè)的過(guò)程就是對(duì)這兩個(gè)頁(yè)面的剪輯。
這里就不贅述翻頁(yè)的原理了(仿真翻頁(yè)可以由貝塞爾曲線計(jì)算坐標(biāo)繪制實(shí)現(xiàn),平移翻頁(yè)則是簡(jiǎn)單坐標(biāo)平移變化),這里提供一些參考鏈接。
現(xiàn)在要做的就是將翻頁(yè)動(dòng)作與 View 結(jié)合起來(lái),我們新建一個(gè) PageAnimController 內(nèi)部實(shí)現(xiàn)翻頁(yè)動(dòng)畫(huà)和動(dòng)畫(huà)切換,同時(shí)設(shè)置 PageCarver 來(lái)監(jiān)聽(tīng)翻頁(yè)動(dòng)作,目的是為了能夠讓 view 檢測(cè)到翻頁(yè)動(dòng)作。
public interface PageCarver {
void drawPage(Canvas canvas, int index);//繪制頁(yè)內(nèi)容
Integer requestPrePage();//請(qǐng)求翻到上一頁(yè)
Integer requestNextPage();//請(qǐng)求翻到下一頁(yè)
void requestInvalidate();//刷新界面
Integer getCurrentPageIndex();//獲取當(dāng)前頁(yè)
/**
* 開(kāi)始動(dòng)畫(huà)的回調(diào)
*
* @param isCancel 是否是取消動(dòng)畫(huà)
*/
void onStartAnim(boolean isCancel);
/**
* 結(jié)束動(dòng)畫(huà)的回調(diào)
*
* @param isCancel 是否是取消動(dòng)畫(huà)
*/
void onStopAnim(boolean isCancel);
}
新建 BaseReaderView 作為閱讀器的基礎(chǔ)視圖,兩者結(jié)合以便控制閱讀器的翻頁(yè)效果。
public abstract class BaseReaderView extends View implements PageAnimController.PageCarver{
/**
* 將View的繪制事件傳送給 PageAnimController 實(shí)現(xiàn)動(dòng)畫(huà)繪制過(guò)程中
* @param canvas
* @return
*/
@Override
protected void onDraw(Canvas canvas) {
if (pageAnimController == null || !pageAnimController.dispatchDrawPage(canvas, this)) {
drawPage(canvas, currentPageIndex);
}
}
/**
* 將View的觸摸事件傳送給 PageAnimController 以便實(shí)現(xiàn)翻頁(yè)動(dòng)畫(huà)
* @param event
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
pageAnimController.dispatchTouchEvent(event, this);
return true;
}
}
但是在翻頁(yè)動(dòng)畫(huà)中是需要無(wú)數(shù)次的調(diào)用 drawPage 來(lái)繪制界面的,為了減少界面計(jì)算的開(kāi)支必須要有一個(gè) Bitmap 緩存來(lái)降低消耗。復(fù)用時(shí)可以直接使用已經(jīng)生成的bitmap.
/**
* <p>
* 頁(yè)面快照,用來(lái)存儲(chǔ)閱讀器每一頁(yè)的內(nèi)容
*
* @author cpacm 2017/10/9
*/
public class PageSnapshot {
private int pageIndex;
private Bitmap mBitmap;
private Canvas mCanvas;
public Canvas beginRecording(int width, int height) {
if (mBitmap == null) {
mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444);
mCanvas = new Canvas(mBitmap);
} else {
mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
}
return mCanvas;
}
public void draw(Canvas canvas) {
if (null != mBitmap) {
canvas.drawBitmap(mBitmap, 0, 0, null);
}
}
public void destroy() {
if (mBitmap != null && !mBitmap.isRecycled()) {
mBitmap.recycle();
mBitmap = null;
}
}
}
基礎(chǔ)模型如下圖所示:

頁(yè)面切換模型
現(xiàn)在我們來(lái)總結(jié)一下,這一部分我們搭建了閱讀器最基礎(chǔ)的框架,包括
(1) 翻頁(yè)動(dòng)畫(huà)與閱讀器視圖的結(jié)合,能夠確保在View中正確監(jiān)聽(tīng)翻頁(yè)動(dòng)作,保證整個(gè)翻頁(yè)動(dòng)作的準(zhǔn)確性。
(2) 利用 Bitmap 緩存優(yōu)化繪圖流程,保證翻頁(yè)動(dòng)畫(huà)的流暢性。而后包括文字,圖片等元素的顯示都是繪制在這個(gè) Bitmap 上的。
書(shū)頁(yè)——組合模式,保證閱讀器高度可定制化

閱讀器模塊圖
一般來(lái)說(shuō),閱讀器獲取數(shù)據(jù)都是一章一章來(lái)的,不管是從網(wǎng)絡(luò)上還是本地。而獲取過(guò)來(lái)的數(shù)據(jù)閱讀器要進(jìn)行分頁(yè)才能展示。如上圖所示,書(shū)頁(yè)展示由 PageElement 模塊負(fù)責(zé),該模塊接收從 BookReaderView 傳入的章節(jié)數(shù)據(jù),然后再經(jīng)底下的4個(gè)模塊計(jì)算來(lái)分頁(yè)。

分頁(yè)模塊
- PageElement,分頁(yè)模塊:功能包括將傳入的章節(jié)數(shù)據(jù)分成數(shù)個(gè) PageData (生成的 PageData 個(gè)數(shù)即為該章節(jié)頁(yè)數(shù),PageData 記錄了每一頁(yè)開(kāi)頭文字在章節(jié)的位置,同時(shí)包含該頁(yè)面HeaderData, LineData,HeadrData 和 FooterData 數(shù)據(jù)等。各個(gè) Data 里面記錄了相應(yīng)的文字信息,可以快速的定位到章節(jié)內(nèi)容中。);繪制頁(yè)面;緩存章節(jié)數(shù)據(jù)以便無(wú)縫切換章節(jié)。
- HeaderElement,頁(yè)頭部分:顯示章節(jié)的標(biāo)題;繪制每一頁(yè)的頭部。
- LineElement,文字行部分:測(cè)量一行文字需要的字?jǐn)?shù);測(cè)量行高;繪制行文字;繪制筆記內(nèi)容;測(cè)量每一個(gè)字在屏幕中的位置,用于筆記功能;
- ImageElement,圖片部分:測(cè)量圖片的寬高;繪制圖片。
- FooterElement,頁(yè)尾部分:繪制每一頁(yè)的頁(yè)尾,包括進(jìn)度,時(shí)間和電量。
//摘自 PageElement 的 onDraw 方法
@Override
public void draw(Canvas canvas) {
int index = drawPageIndex - startPageIndex;
if (index < 0 || index >= pages.size()) return;
BookPageData bookPageData = pages.get(index);
int offsetX = bookSettingParams.paddingLeft;
int offsetY = bookSettingParams.paddingTop;
if (bookPageData == null) return;
canvas.drawColor(bookSettingParams.getBgColor());
bookHeaderElement.setChapterTitle(bookPageData.getChapterName());
bookHeaderElement.setX(offsetX);
bookHeaderElement.setY(offsetY);
if (bookPageData.isChapterFirstPage()) {
bookHeaderElement.drawFirstPage(canvas);
} else {
bookHeaderElement.draw(canvas);
}
bookFooterElement.setProgress(bookPageData.getPageIndex(), bookPageData.getPageNums());
bookFooterElement.setX(offsetX);
bookFooterElement.setY(offsetY + getHeight() - bookFooterElement.getHeight());
bookFooterElement.draw(canvas);
for (int i = 0; i < bookPageData.getDataList().size(); i++) {
BookData bookData = bookPageData.getDataList().get(i);
if (bookData instanceof BookLineData) {
BookLineData bookLineData = (BookLineData) bookData;
bookLineElement.setLineText(bookLineData.getContent());
bookLineElement.setX(bookLineData.getPosition().x);
bookLineElement.setY(bookLineData.getPosition().y);
bookLineElement.drawWithDigests(canvas, bookLineData, bookReaderView.getCurrentDigests(index));
//bookLineElement.draw(canvas);
} else if (bookData instanceof BookImageData) {
BookImageData bookImageData = (BookImageData) bookData;
bookImageElement.setX(bookImageData.getPosition().x);
bookImageElement.setY(bookImageData.getPosition().y);
bookImageElement.syncDrawWithinBitmap(canvas, bookImageData, bookReaderView.getCacheBitmap(drawPageIndex));
}
}
}
將書(shū)頁(yè)分成幾部分組合起來(lái)可以有效的減少代碼的耦合,而且可以自由的控制每一部分的修改,添加和移除。比如當(dāng)以后我想要加個(gè)批注的功能,可以再添加一個(gè)新的 Element ,再?gòu)?fù)寫(xiě)其測(cè)量方法和繪制方法,就可以很方便的使用了。
總結(jié)一下:
(1) PageElement 利用各個(gè) Element 模塊將章節(jié)數(shù)據(jù)進(jìn)行測(cè)量分頁(yè),每一頁(yè) PageData 記錄著 LineData,ImageData,HeaderData和FooterData信息。繪圖時(shí)需要將各個(gè)信息填入 Element 中
(2) 繪圖時(shí)調(diào)用 PageElement 的 draw 方法,其 draw 方法再調(diào)用 各個(gè) Element 的 draw 方法以完成整個(gè)繪圖流程。
另外還需要提到的一點(diǎn)是閱讀器內(nèi)部維護(hù)了一個(gè)書(shū)頁(yè)的隊(duì)列,該隊(duì)列緩存了由三個(gè)章節(jié)數(shù)據(jù)轉(zhuǎn)化而來(lái)的書(shū)頁(yè)列表。比如說(shuō)你正在閱讀第六章,那么隊(duì)列里面緩存的就是第五章,第六章和第七章的數(shù)據(jù),這樣就能實(shí)現(xiàn)上下章翻頁(yè)的無(wú)縫切換而不需要在翻至下一章時(shí)因?yàn)榈却碌恼鹿?jié)數(shù)據(jù)加載而中斷整個(gè)閱讀體驗(yàn)。
/** * <p> * 章節(jié)緩存構(gòu)成方案如下: * | -6,-5,-4,-3,-2,-1,0 | 1,2,3,4,5,6,7,8,9 | 10,11,12,13,14,15 | = pages * | cacheChapter1 | cacheChapter2 | cacheChapter3 | * startPageIndex = pageIndex:-6 endPageIndex = pageIndex:16 * currentChapterStartIndex => pageIndex:1 => pages[7] * currentChapterEndIndex => pageIndex:10 => pages[16] * </p> */
書(shū)簽,筆記——記錄閱讀進(jìn)度

書(shū)簽
書(shū)簽的本質(zhì)就是記錄當(dāng)前頁(yè)的第一個(gè)文字在整章文本的位置,然后再加上書(shū)籍的id,章節(jié)的id(或序號(hào))就能準(zhǔn)確定位。

筆記
要記錄筆記就需要文字選擇器來(lái)選擇文字,這個(gè)時(shí)候就需要知道每一個(gè)字在當(dāng)前的坐標(biāo)位置(之前用 LineElement 測(cè)量文字時(shí)已經(jīng)生成每個(gè)文字的位置)。
為了達(dá)到上圖的效果,就必須要處理在當(dāng)前頁(yè)的觸摸事件:

文字選擇流程
有些細(xì)節(jié)的處理沒(méi)有放到流程中,但大致意思是能明白的
// TextSelectorElement 上的觸摸分發(fā)方法
public boolean dispatchTouchEvent(final MotionEvent ev) {
int key = ev.getAction();
currentTouchPoint.set(ev.getX(), ev.getY());
switch (key) {
case MotionEvent.ACTION_DOWN:
isPressInvalid = false;
hasConsume = true;
isDown = true;
mTouchDownPoint.set(ev.getX(), ev.getY());
// 該方法中會(huì)記錄isBookDigestDown的值
checkIsPressDigests(ev.getX(), ev.getY());
//判斷是否處于選擇模式
if (!isSelect) {
if (isBookDigestDown == 0) {
postLongClickPerform(0);//提交長(zhǎng)按時(shí)間
}
} else {
// 判斷是否觸摸到選擇光標(biāo)上,若是則可以拖動(dòng)光標(biāo)移動(dòng)
checkCurrentMoveCursor(ev);
}
break;
case MotionEvent.ACTION_MOVE:
float move = PointF.length(ev.getX() - mTouchDownPoint.x, ev.getY() - mTouchDownPoint.y);
if (move > moveSlop) {
isPressInvalid = true;
}
if (isPressInvalid) {
removeLongPressPerform();
if (isSelect) {
// 關(guān)閉彈窗(包括筆記編輯框等)
onCloseView();
// 移動(dòng)光標(biāo)
onMove(ev);
} else {
//未處于選擇模式下,相當(dāng)于一個(gè)普通的點(diǎn)擊事件
onPress(ev);
}
}
break;
case MotionEvent.ACTION_UP:
hasConsume = false;
removeLongPressPerform();
if (isSelect) {
// -1 表示為未觸摸到光標(biāo)
if (moveCursor == -1) {
// 取消選擇模式
setSelect(false);
hasConsume = true;
} else {
//停止移動(dòng)時(shí),會(huì)打開(kāi)筆記生成彈框
onOpenDigestsView();
}
moveCursor = -1;
} else {
if (isBookDigestDown == 1) {
onOpenNoteView();
hasConsume = true;
} else if (isBookDigestDown == 2) {
onOpenEditView();
hasConsume = true;
} else {
// 模擬成一個(gè)普通的點(diǎn)擊事件,會(huì)取消當(dāng)前的選擇模式
onPress(ev);
}
}
invalidate();
break;
case MotionEvent.ACTION_CANCEL:
hasConsume = false;
removeLongPressPerform();
break;
default:
break;
}
// 判斷選擇器是否消耗了當(dāng)前事件
return hasConsume || isSelect;
}
當(dāng)然,筆記也要記錄當(dāng)前選擇的書(shū)籍id,章節(jié)id(或序號(hào)),文字在章節(jié)中的位置這些信息,方便定點(diǎn)跳轉(zhuǎn)。
設(shè)置——為閱讀器添磚加瓦

閱讀器設(shè)置界面
閱讀器的設(shè)置一般包括:界面亮度的調(diào)整,字體大小的調(diào)整,上下章的跳轉(zhuǎn),書(shū)籍目錄筆記和書(shū)簽的展示,翻頁(yè)動(dòng)畫(huà)的更改,日夜主題的更改。當(dāng)一些設(shè)置需要閱讀器能夠在參數(shù)變化時(shí)及時(shí)響應(yīng),就得需要在設(shè)置變化時(shí)能及時(shí)更新 BookReaderView 下的各個(gè) Element 模塊。
這里我是通過(guò)一個(gè)輔助類(lèi)貫穿整個(gè)閱讀器來(lái)幫助更新各個(gè)模塊,該類(lèi)記錄了閱讀器內(nèi)部所有可設(shè)置的屬性,當(dāng)各個(gè)模塊被通知需要更新時(shí)重新從該類(lèi)中讀取參數(shù)并設(shè)置(比如畫(huà)筆的顏色,頁(yè)面的間距,字體的大小等)。
// 摘自 PageElement 下的設(shè)置屬性變化方法
// BookSettingParams 即為記錄閱讀器設(shè)置屬性的輔助類(lèi)
@Override
public void update(ReaderSettingParams params) {
bookSettingParams = (BookSettingParams) params;
bookHeaderElement.update(bookSettingParams);
bookFooterElement.update(bookSettingParams);
bookLineElement.update(bookSettingParams);
bookImageElement.update(bookSettingParams);
initPageElement();
}
語(yǔ)音朗讀——為閱讀器添加輔助功能

語(yǔ)音朗讀
此處的語(yǔ)音朗讀使用的是訊飛的TTS引擎。如何使用引入TTS我這里就不具體描述了,重要的是在TTS的 onSpeakProgress(int progress, int beginPos, int endPos) 方法中可以獲取當(dāng)前句子的朗讀進(jìn)度。
當(dāng)我們傳入一章文字時(shí),TTS會(huì)自動(dòng)幫助我們分段(會(huì)以,。等標(biāo)點(diǎn)符號(hào)切割整篇文字),然后按段落來(lái)進(jìn)行朗讀。上面 progress 代表該段落在整篇文字的進(jìn)度,beginPos 代表該段落的起始字符在整篇文字的位置,endPos 代表該段落的末尾字符在整篇文字的位置。
既然能夠知道朗讀的位置,那就能知道朗讀時(shí)文字在屏幕的位置了(之前有說(shuō)過(guò) LineData 記錄了每個(gè)字符在屏幕中的位置),那剩下的就是怎么繪制的問(wèn)題了。
/**
* <p>
* 聽(tīng)書(shū)tts播放模組
*
* @author cpacm 2017/12/13
*/
public class BookSpeechElement extends ResElement implements SynthesizerListener {
// .... 省略部分代碼
// 從每一頁(yè)數(shù)據(jù) PageData 中的 LineData 列表中獲取要繪制的區(qū)域
private void updateDrawRect(int startPos, int endPos) {
if (endPos <= offsetPosition || endPos == this.endPos) return;
this.endPos = endPos;
this.tempPos = startPos;
int s = this.startPos + startPos + bookPageData.getStartPos() - offsetPosition;
int e = this.startPos + endPos + bookPageData.getStartPos() - offsetPosition;
drawRect.clear();
for (BookLineData line : lineData) {
if (line.startPos > e || line.endPos <= s) continue;
if (line.startPos <= s && line.endPos <= e) {
Rect startRect = line.getCharArea().get(s);
Rect endRect = line.getCharArea().get(line.endPos - 1);
Rect rect = new Rect(startRect.left, startRect.top, endRect.right, endRect.bottom);
drawRect.add(rect);
}
if (line.startPos > s && line.endPos <= e) {
Rect startRect = line.getCharArea().get(line.startPos);
Rect endRect = line.getCharArea().get(line.endPos - 1);
Rect rect = new Rect(startRect.left, startRect.top, endRect.right, endRect.bottom);
drawRect.add(rect);
}
if (line.startPos > s && line.endPos > e) {
Rect startRect = line.getCharArea().get(line.startPos);
Rect endRect = line.getCharArea().get(e);
Rect rect = new Rect(startRect.left, startRect.top, endRect.right, endRect.bottom);
drawRect.add(rect);
}
if (line.startPos <= s && line.endPos > e) {
Rect startRect = line.getCharArea().get(s);
Rect endRect = line.getCharArea().get(e);
Rect rect = new Rect(startRect.left, startRect.top, endRect.right, endRect.bottom);
drawRect.add(rect);
}
}
// 刷新當(dāng)前書(shū)頁(yè)
bookReaderView.flashCurrentPageSnapshot();
}
@Override
public void draw(Canvas canvas) {
if (!isSpeaking()) return;
for (Rect rect : drawRect) {
canvas.drawLine(rect.left, rect.bottom, rect.right, rect.bottom, paint);
}
}
@Override
public void destroy() {
exitTts();
}
/*################## 語(yǔ)音合成的回調(diào) ###################*/
@Override
public void onSpeakBegin() {}
@Override
public void onBufferProgress(int progress, int beginPos, int endPos, String info) { }
@Override
public void onSpeakPaused() {}
@Override
public void onSpeakResumed() {}
@Override
public void onSpeakProgress(int progress, int beginPos, int endPos) {
// 根據(jù)朗讀的進(jìn)度更新UI
updateDrawRect(beginPos, endPos);
}
@Override
public void onCompleted(SpeechError speechError) {}
@Override
public void onEvent(int i, int i1, int i2, Bundle bundle) {}
}
總結(jié)
首先聲明一點(diǎn),整篇文章只是闡述了我自己從零開(kāi)始做書(shū)籍閱讀器時(shí)一些思路和使用的一些技巧,并沒(méi)有覆蓋到閱讀器的各個(gè)角落。如果你想要自己實(shí)現(xiàn)一款閱讀器,那你必須要有扎實(shí)的基礎(chǔ)知識(shí),比如View的繪制流程和事件分發(fā)流程,Canvas的繪圖知識(shí)等,這篇文章也只是給大家提個(gè)思路而已。如果有問(wèn)題或者新的想法歡迎交流!
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- Android WebView如何判定網(wǎng)頁(yè)加載的錯(cuò)誤
- Android webView字體突然變小的原因及解決
- Android 解決WebView多進(jìn)程崩潰的方法
- Android 中 WebView 的基本用法詳解
- 在Android環(huán)境下WebView中攔截所有請(qǐng)求并替換URL示例詳解
- 解決Android webview設(shè)置cookie和cookie丟失的問(wèn)題
- Android實(shí)現(xiàn)閱讀進(jìn)度記憶功能
- android閱讀器長(zhǎng)按選擇文字功能實(shí)現(xiàn)代碼
- android仿新聞閱讀器菜單彈出效果實(shí)例(附源碼DEMO下載)
- Android實(shí)現(xiàn)閱讀APP平移翻頁(yè)效果
- Android編程實(shí)現(xiàn)小說(shuō)閱讀器滑動(dòng)效果的方法
- Android使用WebView實(shí)現(xiàn)離線閱讀功能
相關(guān)文章
Android實(shí)現(xiàn)CoverFlow效果控件的實(shí)例代碼
這篇文章主要介紹了Android實(shí)現(xiàn)CoverFlow效果控件的實(shí)例代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-05-05
Android畫(huà)中畫(huà)窗口開(kāi)啟方法
Android8.0 Oreo(API Level26)允許活動(dòng)啟動(dòng)畫(huà)中畫(huà)Picture-in-picture(PIP)模式。PIP是一種特殊類(lèi)型的多窗口模式,主要用于視頻播放。PIP模式已經(jīng)可用于Android TV,而Android8.0則讓該功能可進(jìn)一步用于其他Android設(shè)備2023-01-01
Android實(shí)現(xiàn)可收縮和擴(kuò)展的TextView
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)可收縮和擴(kuò)展的TextView,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03
Android實(shí)現(xiàn)啟動(dòng)引導(dǎo)圖
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)啟動(dòng)引導(dǎo)圖,文中示例代碼介紹的非常詳細(xì),具有為大家詳細(xì)一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-06-06
Android編程實(shí)現(xiàn)保存圖片到系統(tǒng)圖庫(kù)的方法示例
這篇文章主要介紹了Android編程實(shí)現(xiàn)保存圖片到系統(tǒng)圖庫(kù)的方法,結(jié)合實(shí)例形式分析了Android保存圖片到系統(tǒng)圖庫(kù)的常見(jiàn)操作方法、注意事項(xiàng)與相關(guān)問(wèn)題解決技巧,需要的朋友可以參考下2017-08-08
Android?中的監(jiān)聽(tīng)和按鍵處理詳情
這篇文章主要介紹了Android?中的監(jiān)聽(tīng)和按鍵處理詳情,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-08-08
Android實(shí)現(xiàn)網(wǎng)絡(luò)多線程斷點(diǎn)續(xù)傳下載實(shí)例
本示例介紹在Android平臺(tái)下通過(guò)HTTP協(xié)議實(shí)現(xiàn)斷點(diǎn)續(xù)傳下載。具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2016-10-10

