這是一個使用Java(以後還會推出Kotlin版本)語言,從0開發一個Android平臺,接近企業級的專案(我的雲音樂),包含了基礎內容,高階內容,專案封裝,專案重構等知識;主要是使用系統功能,流行的第三方框架,第三方服務,完成接近企業級商業級專案。
隱私協定對話方塊
啟動介面和動態處理許可權
引導介面和廣告
輪播圖和側滑選單
首頁複雜列表和列表排序
音樂播放和音樂列表管理
全域性音樂控制條
桌面歌詞和自定義樣式
全域性媒體控制中心
評論和回覆評論
評論富文字點選
評論提醒人和話題
朋友圈動態列表和釋出
高德地圖定位和路徑規劃
阿里雲OSS上傳
視訊播放和控制
QQ/微信登入和分享
商城/購物車\微信\支付寶支付
文字和圖片聊天
訊息離線推播
自動和手動檢查更新
記憶體漏失和優化
...
2022年5月開發完成的,所以全部都是最新的,平均每3年會重新制作,現在已經是第三版了。
JDK17
Android 12/13
最低相容版本:Android 6.0
Android Studio 2021.1
用最新AS開啟MyCloudMusicAndroidJava目錄,然後等待完全編譯成功,因為是企業級專案,所以第三方依賴很多,同時程式碼量也很多,所以必須要確認完全編譯成功,才能執行。
├── MyCloudMusicAndroidJava
│ ├── LRecyclerview //第三方Recyclerview框架
│ ├── LetterIndexView //類似微信通訊錄字母索引
│ ├── app //雲音樂專案
│ ├── build.gradle
│ ├── common.gradle //通用專案組態檔
│ ├── config //設定目錄,例如簽名
│ ├── glidepalette //Glide畫板,用來從網路圖片提取顏色
│ ├── gradle
│ ├── gradle.properties
│ ├── gradlew
│ ├── gradlew.bat
│ ├── keystore.properties
│ ├── local.properties
│ ├── settings.gradle
│ ├── super-j //公用Java語言擴充套件
│ ├── super-player-tencent //騰訊開源的超級播放器
│ ├── super-speech-baidu //百度語音識別
內容太多,只列出部分。
//分頁元件版本
//這裡可以檢視最新版本:https://developer.android.google.cn/jetpack/androidx/releases/paging
def paging_version = "3.1.1"
//新增所有libs目錄裡面的jar,aar
implementation fileTree(dir: 'libs', include: ['*.jar','*.aar'])
//官方相容元件,像AppCompatActivity就是該依賴裡面的
implementation 'androidx.appcompat:appcompat:1.4.1'
//Material Design元件,像FloatingActionButton就是該依賴裡面的
implementation 'com.google.android.material:material:1.4.0'
//官方提供的約束佈局,像ConstraintLayout就是該依賴裡面的
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'
//UI框架,主要是用他的工具類,也可以單獨拷貝出來
//https://qmuiteam.com/android/get-started
implementation 'com.qmuiteam:qmui:2.0.1'
//動態處理許可權
//https://github.com/permissions-dispatcher/PermissionsDispatcher
implementation "com.github.permissions-dispatcher:permissionsdispatcher:4.8.0"
annotationProcessor "com.github.permissions-dispatcher:permissionsdispatcher-processor:4.8.0"
//api:依賴會傳遞到其他應用本模組的專案
implementation project(path: ':super-j')
...
//使用gson解析json
//https://github.com/google/gson
implementation 'com.google.code.gson:gson:2.9.0'
//自動釋放RxJava相關資源
//https://github.com/uber/AutoDispose
implementation "com.uber.autodispose2:autodispose-androidx-lifecycle:2.1.1"
//banner輪播圖框架
//https://github.com/youth5201314/banner
implementation 'io.github.youth5201314:banner:2.2.2'
//圖片載入框架,還參照他目的是,coil有些功能不好實現
//https://github.com/bumptech/glide
implementation 'com.github.bumptech.glide:glide:+'
annotationProcessor 'com.github.bumptech.glide:compiler:+'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
//給控制元件新增未讀訊息數紅點
//https://github.com/bingoogolapple/BGABadgeView-Android
implementation 'com.github.bingoogolapple.BGABadgeView-Android:api:1.2.0'
annotationProcessor 'com.github.bingoogolapple.BGABadgeView-Android:compiler:1.2.0'
//webview進度條
//https://github.com/youlookwhat/WebProgress
implementation 'com.github.youlookwhat:WebProgress:1.2.0'
//紀錄檔框架
//https://github.com/JakeWharton/timber
implementation 'com.jakewharton.timber:timber:5.0.1'
implementation "androidx.media:media:+"
//和Glide配合處理圖片
//可以實現很多效果
//模糊;圓角;圓
//我們這裡是用它實現模糊效果
//https://github.com/wasabeef/glide-transformations
implementation 'jp.wasabeef:glide-transformations:+'
//圓形圖片控制元件
//https://github.com/hdodenhof/CircleImageView
implementation 'de.hdodenhof:circleimageview:+'
//下載框架
//https://github.com/ixuea/android-downloader
implementation 'com.ixuea:android-downloader:3.0.0'
//阿里雲oss
//官方檔案:https://help.aliyun.com/document_detail/32043.html
//sdk地址:https://github.com/aliyun/aliyun-oss-android-sdk
implementation 'com.aliyun.dpa:oss-android-sdk:+'
//高德地圖,這裡參照的是3d
//https://lbs.amap.com/api/android-sdk/guide/create-project/android-studio-create-project#gradle_sdk
implementation 'com.amap.api:3dmap:+'
//定位功能
implementation 'com.amap.api:location:+'
//百度語音相關技術,目前主要用在收貨地址編輯介面,語音輸入收貨地址
//https://ai.baidu.com/ai-doc/SPEECH/Pkgt4wwdx#%E9%9B%86%E6%88%90%E6%8C%87%E5%8D%97
implementation project(path: ':super-speech-baidu')
//TextView顯示富文字,目前主要用在商品詳情介面,顯示富文字商品描述
//https://github.com/wangchenyan/html-text
implementation 'com.github.wangchenyan:html-text:+'
//Hutool是一個小而全的Java工具類庫
// 通過靜態方法封裝,降低相關API的學習成本
// 提高工作效率,使Java擁有函數式語言般的優雅
//https://github.com/looly/hutool
implementation 'cn.hutool:hutool-all:5.7.14'
//支付寶支付
//https://opendocs.alipay.com/open/204/105296
implementation 'com.alipay.sdk:alipaysdk-android:+@aar'
//融雲IM
//https://docs.rongcloud.cn/v4/5X/views/im/ui/guide/quick/include/android.html
implementation 'cn.rongcloud.sdk:im_lib:+'
//微信支付
//官方sdk下載檔案:https://developers.weixin.qq.com/doc/oplatform/Downloads/Android_Resource.html
//官方整合檔案:https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=8_5
implementation 'com.tencent.mm.opensdk:wechat-sdk-android:+'
//記憶體漏失檢測工具
//https://github.com/square/leakcanary
//只有偵錯模式下才新增該依賴
debugImplementation 'com.squareup.leakcanary:leakcanary-android:+'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
使用自定義DialogFragment實現,內容是放到字串檔案中的,其中的連結是HTML標籤,設定後就可以點選了,然後修改預設對話方塊寬度,因為預設的有點窄。
public class TermServiceDialogFragment extends BaseViewModelDialogFragment<FragmentDialogTermServiceBinding> {
...
@Override
protected void initViews() {
super.initViews();
//點選彈窗外邊不能關閉
setCancelable(false);
SuperTextUtil.setLinkColor(binding.content, getActivity().getColor(R.color.link));
}
@Override
protected void initListeners() {
super.initListeners();
binding.primary.setOnClickListener(view -> {
dismiss();
onAgreementClickListener.onClick(view);
});
binding.disagree.setOnClickListener(view -> {
dismiss();
SuperProcessUtil.killApp();
});
}
@Override
public void onResume() {
super.onResume();
//修改寬度,預設比AlertDialog.Builder顯示對話方塊寬度窄,看著不好看
//參考:https://stackoverflow.com/questions/12478520/how-to-set-dialogfragments-width-and-height
ViewGroup.LayoutParams params = getDialog().getWindow().getAttributes();
params.width = (int) (ScreenUtil.getScreenWith(getContext()) * 0.9);
params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
getDialog().getWindow().setAttributes((android.view.WindowManager.LayoutParams) params);
}
}
高版本必須要動態處理許可權,這裡在啟動介面請求了一些許可權,但推薦在用到的時候才獲取,寫法差不多,這裡使用第三方框架實現,當然也可以直接使用系統API實現。
/**
* 許可權授權了就會呼叫該方法
* 請求相機許可權目的是掃描二維條碼,拍照
*/
@NeedsPermission({
Manifest.permission.CAMERA,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION
})
void onPermissionGranted() {
//如果有許可權就進入下一步
prepareNext();
}
/**
* 顯示許可權授權對話方塊
* 目的是提示使用者
*/
@OnShowRationale({
Manifest.permission.CAMERA,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION
})
void showRequestPermission(PermissionRequest request) {
new AlertDialog.Builder(getHostActivity())
.setMessage(R.string.permission_hint)
.setPositiveButton(R.string.allow, (dialog, which) -> request.proceed())
.setNegativeButton(R.string.deny, (dialog, which) -> request.cancel()).show();
}
/**
* 拒絕了許可權呼叫
*/
@OnPermissionDenied({
Manifest.permission.CAMERA,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION
})
void showDenied() {
//退出應用
finish();
}
/**
* 再次獲取許可權的提示
*/
@OnNeverAskAgain({
Manifest.permission.CAMERA,
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION
})
void showNeverAsk() {
//繼續請求許可權
checkPermission();
}
/**
* 授權後回撥
*
* @param requestCode
* @param permissions
* @param grantResults
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
//將授權結果傳遞到框架
SplashActivityPermissionsDispatcher.onRequestPermissionsResult(this, requestCode, grantResults);
}
引導介面比較簡單,就是多個圖片可以左右捲動,整體使用ViewPager+Fragment實現,也可以使用ViewPager2,後面有講解。
/**
* 引導介面介面卡
*/
public class GuideAdapter extends BaseFragmentStatePagerAdapter<Integer> {
/***
* @param context 上下文
* @param fm Fragment管理器
*/
public GuideAdapter(Context context, @NonNull FragmentManager fm) {
super(context, fm);
}
/**
* 返回當前位置Fragment
*
* @param position
* @return
*/
@NonNull
@Override
public Fragment getItem(int position) {
return GuideFragment.newInstance(getData(position));
}
}
/**
* 引導介面Fragment
*/
public class GuideFragment extends BaseViewModelFragment<FragmentGuideBinding> {
...
@Override
protected void initDatum() {
super.initDatum();
int data = getArguments().getInt(Constant.ID);
binding.icon.setImageResource(data);
}
}
實現圖片廣告和視訊廣告,廣告資料是在首頁是快取到本地,目的是在啟動介面載入更快,因為真實專案中,大部分專案啟動頁面廣告時間一共就5秒,如果太長了使用者體驗不好,如果是從網路請求,那麼網路可能就耗時2秒左右,所以導致就美喲多少時間顯示廣告了。
private void downloadAd(Ad data) {
if (SuperNetworkUtil.isWifiConnected(getHostActivity())) {
//wifi才下載
sp.setSplashAd(data);
//判斷檔案是否存在,如果存在就不下載
File targetFile = FileUtil.adFile(getHostActivity(), data.getIcon());
if (targetFile.exists()) {
return;
}
new Thread(
new Runnable() {
@Override
public void run() {
try {
//FutureTarget會阻塞
//所以需要在子執行緒呼叫
FutureTarget<File> target = Glide.with(getHostActivity().getApplicationContext())
.asFile()
.load(ResourceUtil.resourceUri(data.getIcon()))
.submit();
//獲取下載的檔案
File file = target.get();
//將檔案拷貝到我們需要的位置
FileUtils.moveFile(file, targetFile);
} catch (Exception e) {
e.printStackTrace();
}
}
}
).start();
}
}
/**
* 顯示視訊廣告
*
* @param data
*/
private void showVideoAd(File data) {
SuperViewUtil.show(binding.video);
SuperViewUtil.show(binding.preload);
//在要用到的時候在初始化,更節省資源,當然播放器控制元件也可以在這裡動態建立
//設定播放監聽器
//建立 player 物件
player = new TXVodPlayer(getHostActivity());
//靜音,當然也可以在介面上新增靜音切換按鈕
player.setMute(true);
//關鍵 player 物件與介面 view
player.setPlayerView(binding.video);
//設定播放監聽器
player.setVodListener(this);
//鋪滿
binding.video.setRenderMode(TXLiveConstants.RENDER_MODE_FULL_FILL_SCREEN);
//開啟硬體加速
player.enableHardwareDecode(true);
player.startPlay(data.getAbsolutePath());
}
顯示圖片就是顯示本地圖片了,沒什麼難點,就不貼程式碼了。
首頁沒有頂部是輪播圖,然後是可以左右的選單,接下來是熱門歌單,推薦單曲,最後是首頁排序模組;整體上使用RecycerView實現,輪播圖:
Banner bannerView = holder.getView(R.id.banner);
BannerImageAdapter<Ad> bannerImageAdapter = new BannerImageAdapter<Ad>(data.getData()) {
@Override
public void onBindView(BannerImageHolder holder, Ad data, int position, int size) {
ImageUtil.show(getContext(), (ImageView) holder.itemView, data.getIcon());
}
};
bannerView.setAdapter(bannerImageAdapter);
bannerView.setOnBannerListener(onBannerListener);
bannerView.setBannerRound(DensityUtil.dip2px(getContext(), 10));
//新增生命週期觀察者
bannerView.addBannerLifecycleObserver(fragment);
bannerView.setIndicator(new CircleIndicator(getContext()));
推薦歌單
//設定標題,將標題放到每個具體的item上,好處是方便整體排序
holder.setText(R.id.title, R.string.recommend_sheet);
//顯示更多容器
holder.setVisible(R.id.more, true);
holder.getView(R.id.more).setOnClickListener(v -> {
});
RecyclerView listView = holder.getView(R.id.list);
if (listView.getAdapter() == null) {
//設定顯示3列
GridLayoutManager layoutManager = new GridLayoutManager(listView.getContext(), 3);
listView.setLayoutManager(layoutManager);
sheetAdapter = new SheetAdapter(R.layout.item_sheet);
//item點選
sheetAdapter.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(@NonNull BaseQuickAdapter<?, ?> adapter, @NonNull View view, int position) {
if (discoveryAdapterListener != null) {
discoveryAdapterListener.onSheetClick((Sheet) adapter.getItem(position));
}
}
});
listView.setAdapter(sheetAdapter);
GridDividerItemDecoration itemDecoration = new GridDividerItemDecoration(getContext(), (int) DensityUtil.dip2px(getContext(), 5F));
listView.addItemDecoration(itemDecoration);
}
sheetAdapter.setNewInstance(data.getData());
頂部是歌單資訊,通過header實現,底部是列表,顯示歌單內容的音樂,點選音樂進入黑膠唱片播放介面。
//新增頭部
adapter.addHeaderView(createHeaderView());
/**
* 顯示資料的方法
*
* @param holder
* @param data
*/
@Override
protected void convert(@NonNull BaseViewHolder holder, Song data) {
//顯示位置
holder.setText(R.id.index, String.valueOf(holder.getLayoutPosition() + offset));
//顯示標題
holder.setText(R.id.title, data.getTitle());
//顯示資訊
holder.setText(R.id.info, data.getSinger().getNickname());
if (offset != 0) {
holder.setImageResource(R.id.more, R.drawable.close);
holder.getView(R.id.more)
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SuperDialog.newInstance(fragmentManager)
.setTitleRes(R.string.confirm_delete)
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//查詢下載任務
DownloadInfo downloadInfo = AppContext.getInstance().getDownloadManager().getDownloadById(data.getId());
if (downloadInfo != null) {
//從下載框架刪除
AppContext.getInstance().getDownloadManager().remove(downloadInfo);
} else {
AppContext.getInstance().getOrm().deleteSong(data);
}
//從介面卡中刪除
removeAt(holder.getAdapterPosition());
}
}).show();
}
});
} else {
//是否下載
DownloadInfo downloadInfo = AppContext.getInstance().getDownloadManager().getDownloadById(data.getId());
if (downloadInfo != null && downloadInfo.getStatus() == DownloadInfo.STATUS_COMPLETED) {
//下載完成了
//顯示下載完成了圖示
holder.setGone(R.id.download, false);
} else {
holder.setGone(R.id.download, true);
}
}
//處理編輯狀態
if (isEditing()) {
holder.setVisible(R.id.index, false);
holder.setVisible(R.id.check, true);
holder.setVisible(R.id.more, false);
if (isSelected(holder.getLayoutPosition())) {
holder.setImageResource(R.id.check, R.drawable.ic_checkbox_selected);
} else {
holder.setImageResource(R.id.check, R.drawable.ic_checkbox);
}
} else {
holder.setVisible(R.id.index, true);
holder.setVisible(R.id.check, false);
holder.setVisible(R.id.more, true);
}
}
上面是黑膠唱片,和網易雲音樂差不多,隨著音樂捲動或暫停,頂部是控制相關,音樂播放邏輯是封裝到MusicPlayerManager中:
/**
* 播放管理器預設實現
*/
public class MusicPlayerManagerImpl implements MusicPlayerManager, MediaPlayer.OnCompletionListener, AudioManager.OnAudioFocusChangeListener {
...
/**
* 獲取播放管理器
* getInstance:方法名可以隨便取
* 只是在Java這邊大部分專案都取這個名字
*
* @return
*/
public synchronized static MusicPlayerManager getInstance(Context context) {
if (instance == null) {
instance = new MusicPlayerManagerImpl(context);
}
return instance;
}
@Override
public void play(String uri, Song data) {
//儲存資訊
this.uri = uri;
this.data = data;
//釋放播放器
player.reset();
//獲取音訊焦點
if (!requestAudioFocus()) {
return;
}
playNow();
}
private void playNow() {
isPrepare = true;
try {
if (uri.startsWith("content://")) {
//內容提供者格式
//本地音樂
//uri範例:content://media/external/audio/media/23
player.setDataSource(context, Uri.parse(uri));
} else {
//設定資料來源
player.setDataSource(uri);
}
//同步準備
//真實專案中可能會使用非同步
//因為如果網路不好
//同步可能會卡住
player.prepare();
// player.prepareAsync();
//開始播放器
player.start();
//回撥監聽器
publishPlayingStatus();
//啟動播放進度通知
startPublishProgress();
prepareLyric(data);
} catch (IOException e) {
//TODO 播放錯誤處理
}
}
@Override
public void pause() {
if (isPlaying()) {
//如果在播放就暫停
player.pause();
ListUtil.eachListener(listeners, musicPlayerListener -> musicPlayerListener.onPaused(data));
stopPublishProgress();
}
}
@Override
public void resume() {
if (!isPlaying()) {
//獲取音訊焦點
if (!requestAudioFocus()) {
return;
}
resumeNow();
}
}
private void resumeNow() {
//如果沒有播放就播放
player.start();
//回撥監聽器
publishPlayingStatus();
//啟動進度通知
startPublishProgress();
}
@Override
public void addMusicPlayerListener(MusicPlayerListener listener) {
if (!listeners.contains(listener)) {
listeners.add(listener);
}
//啟動進度通知
startPublishProgress();
}
@Override
public void removeMusicPlayerListener(MusicPlayerListener listener) {
listeners.remove(listener);
}
@Override
public void seekTo(int progress) {
player.seekTo(progress);
}
/**
* 釋出播放中狀態
*/
private void publishPlayingStatus() {
// for (MusicPlayerListener listener : listeners) {
// listener.onPlaying(data);
// }
//使用重構後的方法
ListUtil.eachListener(listeners, musicPlayerListener -> musicPlayerListener.onPlaying(data));
}
/**
* 播放完畢了回撥
*
* @param mp
*/
@Override
public void onCompletion(MediaPlayer mp) {
isPrepare = false;
//回撥監聽器
ListUtil.eachListener(listeners, listener -> listener.onCompletion(mp));
}
@Override
public void setLooping(boolean looping) {
player.setLooping(looping);
}
/**
* 音訊焦點改變了回撥
*
* @param focusChange
*/
@Override
public void onAudioFocusChange(int focusChange) {
Timber.d("onAudioFocusChange %s", focusChange);
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
//獲取到焦點了
if (resumeOnFocusGain) {
if (isPrepare) {
resumeNow();
} else {
playNow();
}
resumeOnFocusGain = false;
}
break;
case AudioManager.AUDIOFOCUS_LOSS:
//永久失去焦點,例如:其他應用請求時,也是播放音樂
if (isPlaying()) {
pause();
}
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
//暫時性失去焦點,例如:通話了,或者呼叫了語音助手等請求
if (isPlaying()) {
resumeOnFocusGain = true;
pause();
}
break;
}
}
}
音樂列表邏輯封裝到MusicListManager:
public class MusicListManagerImpl implements MusicListManager, MusicPlayerListener {
@Override
public void setDatum(List<Song> datum) {
//將原來資料playList標誌設定為false
DataUtil.changePlayListFlag(this.datum, false);
//儲存到資料庫
saveAll();
//清空原來的資料
this.datum.clear();
//新增新的資料
this.datum.addAll(datum);
//更改播放列表標誌
DataUtil.changePlayListFlag(this.datum, true);
//儲存到資料庫
saveAll();
sendPlayListChangedEvent(0);
}
/**
* 儲存播放列表
*/
private void saveAll() {
getOrm().saveAll(datum);
}
private LiteORMUtil getOrm() {
return LiteORMUtil.getInstance(this.context);
}
@Override
public void play(Song data) {
//當前音樂黑膠唱片捲動
data.setRotate(true);
//標記已經播放了
isPlay = true;
//儲存資料
this.data = data;
if (StringUtils.isNotBlank(data.getPath())) {
//本地音樂
//不拼接地址
musicPlayerManager.play(data.getPath(), data);
} else {
//判斷是否有下載物件
DownloadInfo downloadInfo = AppContext.getInstance().getDownloadManager().getDownloadById(data.getId());
if (downloadInfo != null && downloadInfo.getStatus() == DownloadInfo.STATUS_COMPLETED) {
//下載完成了
//播放本地音樂
musicPlayerManager.play(downloadInfo.getPath(), data);
Timber.d("play offline %s %s %s", data.getTitle(), downloadInfo.getPath(), data.getUri());
} else {
//播放線上音樂
String path = ResourceUtil.resourceUri(data.getUri());
musicPlayerManager.play(path, data);
Timber.d("play online %s %s", data.getTitle(), path);
}
}
//設定最後播放音樂的Id
sp.setLastPlaySongId(data.getId());
}
@Override
public void pause() {
musicPlayerManager.pause();
}
@Override
public Song next() {
if (datum.size() == 0) {
//如果沒有音樂了
//直接返回null
return null;
}
//音樂索引
int index = 0;
//判斷迴圈模式
switch (model) {
case MODEL_LOOP_RANDOM:
//隨機迴圈
//在0~datum.size()中
//不包含datum.size()
index = new Random().nextInt(datum.size());
break;
default:
//找到當前音樂索引
index = datum.indexOf(data);
if (index != -1) {
//找到了
//如果當前播放是列表最後一個
if (index == datum.size() - 1) {
//最後一首音樂
//那就從0開始播放
index = 0;
} else {
index++;
}
} else {
//丟擲異常
//因為正常情況下是能找到的
throw new IllegalArgumentException("Cant'found current song");
}
break;
}
return datum.get(index);
}
@Override
public void delete(int position) {
//獲取要刪除的音樂
Song song = datum.get(position);
if (song.getId().equals(data.getId())) {
//刪除的音樂就是當前播放的音樂
//應該停止當前播放
pause();
//並播放下一首音樂
Song next = next();
if (next.getId().equals(data.getId())) {
//找到了自己
//沒有歌曲可以播放了
data = null;
//TODO Bug 隨機迴圈的情況下有可能獲取到自己
} else {
play(next);
}
}
//直接刪除
datum.remove(song);
//從資料庫中刪除
getOrm().deleteSong(song);
sendPlayListChangedEvent(position);
}
private void sendPlayListChangedEvent(int position) {
EventBus.getDefault().post(new MusicPlayListChangedEvent(position));
}
/**
* 播放完畢了回撥
*
* @param mp
*/
@Override
public void onCompletion(MediaPlayer mp) {
if (model == MODEL_LOOP_ONE) {
//如果是單曲迴圈
//就不會處理了
//因為我們使用了MediaPlayer的迴圈模式
//如果使用的第三方框架
//如果沒有迴圈模式
//那就要在這裡繼續播放當前音樂
} else {
Song data = next();
if (data != null) {
play(data);
}
}
}
...
}
外界統一使用播放列表管理器播放音樂,上一曲下一曲:
//播放按鈕點選
binding.play.setOnClickListener(v -> {
playOrPause();
});
//下一曲按鈕點選
binding.next.setOnClickListener(v -> {
getMusicListManager().play(getMusicListManager().next());
});
//播放列表按鈕點選
binding.listButton.setOnClickListener(v -> {
MusicPlayListDialogFragment.show(getSupportFragmentManager());
});
歌詞實現了LRC,KSC兩種歌詞,封裝到LyricListView,單個歌詞行封裝到LyricView中,外界直接使用LyricListView就行:
private void showLyricData() {
binding.lyricList.setData(getMusicListManager().getData().getParsedLyric());
}
桌面歌詞使用兩個LyricView顯示兩行歌詞,桌面歌詞使用的是全域性懸浮窗API,所以要先判斷是否有許可權,沒有需要先獲取許可權,然後才能顯示,封裝到GlobalLyricManagerImpl中:
/**
* 全域性(桌面)歌詞管理器實現
*/
public class GlobalLyricManagerImpl implements GlobalLyricManager, MusicPlayerListener, GlobalLyricView.OnGlobalLyricDragListener, GlobalLyricView.GlobalLyricListener {
public GlobalLyricManagerImpl(Context context) {
this.context = context.getApplicationContext();
//初始化偏好設定工具類
sp = PreferenceUtil.getInstance(this.context);
//初始化音樂播放管理器
musicPlayerManager = MusicPlayerService.getMusicPlayerManager(this.context);
//新增播放監聽器
musicPlayerManager.addMusicPlayerListener(this);
//初始化視窗管理器
initWindowManager();
//從偏好設定中獲取是否要顯示全域性歌詞
if (sp.isShowGlobalLyric()) {
//建立全域性歌詞View
initGlobalLyricView();
//如果原來鎖定了歌詞
if (sp.isGlobalLyricLock()) {
//鎖定歌詞
lock();
}
}
}
public synchronized static GlobalLyricManagerImpl getInstance(Context context) {
if (instance == null) {
instance = new GlobalLyricManagerImpl(context);
}
return instance;
}
/**
* 鎖定全域性歌詞
*/
private void lock() {
//儲存全域性歌詞鎖定狀態
sp.setGlobalLyricLock(true);
//設定全域性歌詞控制元件狀態
setGlobalLyricStatus();
//顯示簡單模式
globalLyricView.simpleStyle();
//更新佈局
updateView();
//顯示解鎖全域性歌詞通知
NotificationUtil.showUnlockGlobalLyricNotification(context);
//註冊接收解鎖全域性歌詞廣告接收器
registerUnlockGlobalLyricReceiver();
}
/**
* 註冊接收解鎖全域性歌詞廣告接收器
*/
private void registerUnlockGlobalLyricReceiver() {
if (unlockGlobalLyricBroadcastReceiver == null) {
//建立廣播接受者
unlockGlobalLyricBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (Constant.ACTION_UNLOCK_LYRIC.equals(intent.getAction())) {
//歌詞解鎖事件
unlock();
}
}
};
IntentFilter intentFilter = new IntentFilter();
//只監聽歌詞解鎖事件
intentFilter.addAction(Constant.ACTION_UNLOCK_LYRIC);
//註冊
context.registerReceiver(unlockGlobalLyricBroadcastReceiver, intentFilter);
}
}
/**
* 解鎖歌詞
*/
private void unlock() {
//設定沒有鎖定歌詞
sp.setGlobalLyricLock(false);
//設定歌詞狀態
setGlobalLyricStatus();
//解鎖後顯示標準樣式
globalLyricView.normalStyle();
//更新view
updateView();
//清除歌詞解鎖通知
NotificationUtil.clearUnlockGlobalLyricNotification(context);
//解除接收全域性歌詞事件廣播接受者
unregisterUnlockGlobalLyricReceiver();
}
/**
* 解除接收全域性歌詞事件廣播接受者
*/
private void unregisterUnlockGlobalLyricReceiver() {
if (unlockGlobalLyricBroadcastReceiver != null) {
context.unregisterReceiver(unlockGlobalLyricBroadcastReceiver);
unlockGlobalLyricBroadcastReceiver = null;
}
}
@Override
public void show() {
//檢查全域性懸浮窗許可權
if (!Settings.canDrawOverlays(context)) {
Intent intent = new Intent(context, SplashActivity.class);
intent.setAction(Constant.ACTION_LYRIC);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
return;
}
//初始化全域性歌詞控制元件
initGlobalLyricView();
//設定顯示了全域性歌詞
sp.setShowGlobalLyric(true);
WidgetUtil.onGlobalLyricShowStatusChanged(context, isShowing());
}
private boolean hasGlobalLyricView() {
return globalLyricView != null;
}
/**
* 全域性歌詞拖拽回撥
*
* @param y y軸方向上移動的距離
*/
@Override
public void onGlobalLyricDrag(int y) {
layoutParams.y = y - SizeUtil.getStatusBarHeight(context);
//更新view
updateView();
//儲存歌詞y座標
sp.setGlobalLyricViewY(layoutParams.y);
}
...
}
顯示和隱藏只需要呼叫該管理器的相關方法就行了。
使用了可以通過系統媒體控制器,通知欄,鎖屏介面,耳機,藍芽耳機等裝置控制媒體播放暫停,只需要把媒體資訊更新到系統:
MusicPlayerService
/**
* 更新媒體資訊
*
* @param data
* @param icon
*/
public void updateMetaData(Song data, Bitmap icon) {
MediaMetadataCompat.Builder metaData = new MediaMetadataCompat.Builder()
//標題
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, data.getTitle())
//藝術家,也就是歌手
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, data.getSinger().getNickname())
//專輯
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, "專輯")
//專輯藝術家
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, "專輯藝術家")
//時長
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, data.getDuration())
//封面
.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, icon);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
//播放列表長度
metaData.putLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS, musicListManager.getDatum().size());
}
mediaSession.setMetadata(metaData.build());
}
/**
* 媒體回撥
*/
private MediaSessionCompat.Callback callback = new MediaSessionCompat.Callback() {
@Override
public void onPlay() {
musicListManager.resume();
}
@Override
public void onPause() {
musicListManager.pause();
}
@Override
public void onSkipToNext() {
musicListManager.play(musicListManager.next());
}
@Override
public void onSkipToPrevious() {
musicListManager.play(musicListManager.previous());
}
@Override
public void onSeekTo(long pos) {
musicListManager.seekTo((int) pos);
}
};
建立佈局,然後註冊,最後就是更新資訊:
public class MusicWidget extends AppWidgetProvider {
/**
* 新增,重新執行應用,週期時間,都會呼叫
*
* @param context
* @param appWidgetManager
* @param appWidgetIds
*/
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
super.onUpdate(context, appWidgetManager, appWidgetIds);
//嘗試啟動service
ServiceUtil.startService(context.getApplicationContext(), MusicPlayerService.class);
//獲取播放列表管理器
MusicListManager musicListManager = MusicPlayerService.getListManager(context.getApplicationContext());
//獲取當前播放的音樂
final Song data = musicListManager.getData();
final int N = appWidgetIds.length;
// 迴圈處理每一個,因為桌面上可能新增多個
for (int i = 0; i < N; i++) {
int appWidgetId = appWidgetIds[i];
// 建立遠端控制元件,所有對view的操作都必須通過該view提供的方法
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.music_widget);
//因為這是在桌面的控制元件裡面顯示我們的控制元件,所以不能直接通過setOnClickListener設定監聽器
//這裡傳送的動作在MusicReceiver處理
PendingIntent iconPendingIntent = IntentUtil.createMainActivityPendingIntent(context, Constant.ACTION_MUSIC_PLAYER_PAGE);
//這裡直接啟動service,也可以用廣播接收
PendingIntent previousPendingIntent = IntentUtil.createMusicPlayerServicePendingIntent(context, Constant.ACTION_PREVIOUS);
PendingIntent playPendingIntent = IntentUtil.createMusicPlayerServicePendingIntent(context, Constant.ACTION_PLAY);
PendingIntent nextPendingIntent = IntentUtil.createMusicPlayerServicePendingIntent(context, Constant.ACTION_NEXT);
PendingIntent lyricPendingIntent = IntentUtil.createMusicPlayerServicePendingIntent(context, Constant.ACTION_LYRIC);
//設定點選事件
views.setOnClickPendingIntent(R.id.icon, iconPendingIntent);
views.setOnClickPendingIntent(R.id.previous, previousPendingIntent);
views.setOnClickPendingIntent(R.id.play, playPendingIntent);
views.setOnClickPendingIntent(R.id.next, nextPendingIntent);
views.setOnClickPendingIntent(R.id.lyric, lyricPendingIntent);
if (data == null) {
//當前沒有播放音樂
appWidgetManager.updateAppWidget(appWidgetId, views);
} else {
//有播放音樂
views.setTextViewText(R.id.title, String.format("%s - %s", data.getTitle(), data.getSinger().getNickname()));
views.setProgressBar(R.id.progress, (int) data.getDuration(), (int) data.getProgress(), false);
//顯示圖示
RequestOptions options = new RequestOptions();
options.centerCrop();
Glide.with(context)
.asBitmap()
.load(ResourceUtil.resourceUri(data.getIcon()))
.apply(options)
.into(new CustomTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
//顯示封面
views.setImageViewBitmap(R.id.icon, resource);
appWidgetManager.updateAppWidget(appWidgetId, views);
}
@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
//顯示預設圖片
views.setImageViewBitmap(R.id.icon, BitmapFactory.decodeResource(context.getResources(), R.drawable.placeholder));
appWidgetManager.updateAppWidget(appWidgetId, views);
}
});
}
}
}
}
登入註冊沒有多大難度,使用者名稱和密碼登入,就是把資訊傳遞到伺服器端,可以加密後在傳輸,伺服器端判斷登入成功,返回一個標記,使用者端儲存,其他需要的登入的介面帶上;驗證碼登入就是用驗證碼代替密碼,傳送驗證碼都是伺服器端傳送,使用者端只需要呼叫介面。
評論列表包括下拉重新整理,上拉載入更多,點贊,釋出評論,回覆評論,Emoji,話題和提醒人點選,選擇好友,選擇話題等。
核心邏輯就只需要更改page就行了
//下拉重新整理監聽器
binding.refresh.setOnRefreshListener(new OnRefreshListener() {
@Override
public void onRefresh(RefreshLayout refreshlayout) {
loadData();
}
});
//上拉載入更多
binding.refresh.setOnLoadMoreListener(new OnLoadMoreListener() {
@Override
public void onLoadMore(RefreshLayout refreshlayout) {
loadMore();
}
});
@Override
protected void loadData(boolean isPlaceholder) {
super.loadData(isPlaceholder);
isRefresh = true;
pageMeta = null;
loadMore();
}
通過正規表示式,找到特殊文字,然後使用富文字實現點選。
holder.setText(R.id.content, processContent(data.getContent()));
/**
* 處理文字點選事件
* 這部分可以用監聽器回撥到Activity中處理
*
* @param content
* @return
*/
private SpannableString processContent(String content) {
//設定點選事件
SpannableString result = RichUtil.processContent(getContext(), content,
new RichUtil.OnTagClickListener() {
@Override
public void onTagClick(String data, RichUtil.MatchResult matchResult) {
String clickText = RichUtil.removePlaceholderString(data);
Timber.d("processContent mention click %s", clickText);
UserDetailActivity.startWithNickname(getContext(), clickText);
}
},
(data, matchResult) -> {
String clickText = RichUtil.removePlaceholderString(data);
Timber.d("processContent hash tag %s", clickText);
});
//返回結果
return result;
}
對資料分組,然後顯示右側索引,選擇了通過EventBus傳送到評論介面。
adapter.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(@NonNull BaseQuickAdapter<?, ?> adapter, @NonNull View view, int position) {
Object data = adapter.getItem(position);
if (data instanceof User) {
if (Constant.STYLE_FRIEND_SELECT == style) {
EventBus.getDefault().post(new SelectedFriendEvent((User) data));
//關閉介面
finish();
} else {
startActivityExtraId(UserDetailActivity.class, ((User) data).getId());
}
}
}
});
}
真實專案中視訊播放大部分都是用第三方服務,例如:阿里雲視訊服務,騰訊視訊服務,因為他們提供一條龍服務,包括稽核,轉碼,CDN,安全,播放器等,這裡用不到這麼多功能,所以使用了第三方播放器播放普通mp4,這使用餃子播放器框架。
GSYVideoOptionBuilder videoOption = new GSYVideoOptionBuilder();
videoOption
// .setThumbImageView(imageView)
//小屏時不觸控滑動
.setIsTouchWiget(false)
//音訊焦點衝突時是否釋放
.setReleaseWhenLossAudio(true)
.setRotateViewAuto(false)
.setLockLand(false)
.setAutoFullWithSize(true)
.setSeekOnStart(seek)
.setNeedLockFull(true)
.setUrl(ResourceUtil.resourceUri(data.getUri()))
.setCacheWithPlay(false)
//全螢幕切換時不使用動畫
.setShowFullAnimation(false)
.setVideoTitle(data.getTitle())
//設定右下角 顯示切換到全螢幕 的按鍵資源
.setEnlargeImageRes(R.drawable.full_screen)
//設定右下角 顯示退出全螢幕 的按鍵資源
.setShrinkImageRes(R.drawable.normal_screen)
.setVideoAllCallBack(new GSYSampleCallBack() {
@Override
public void onPrepared(String url, Object... objects) {
super.onPrepared(url, objects);
//開始播放了才能旋轉和全螢幕
orientationUtils.setEnable(true);
isPlay = true;
}
@Override
public void onQuitFullscreen(String url, Object... objects) {
super.onQuitFullscreen(url, objects);
if (orientationUtils != null) {
orientationUtils.backToProtVideo();
}
}
}).setLockClickListener(new LockClickListener() {
@Override
public void onClick(View view, boolean lock) {
if (orientationUtils != null) {
//配合下方的onConfigurationChanged
orientationUtils.setEnable(!lock);
}
}
}).build(binding.player);
//開始播放
binding.player.startPlayLogic();
使用者詳情頂部顯示使用者資訊,好友數量,下面分別顯示建立的歌單,收藏的歌單,釋出的動態,類似微信朋友圈,右上角可以更改使用者資料;整體採用CoordinatorLayout+TabLayout+ViewPager+Fragment實現。
public Fragment getItem(int position) {
switch (position) {
case 0:
return UserDetailSheetFragment.newInstance(userId);
case 1:
return FeedFragment.newInstance(userId);
default:
return UserDetailAboutFragment.newInstance(userId);
}
}
/**
* 返回標題
*
* @param position
* @return
*/
@Nullable
@Override
public CharSequence getPageTitle(int position) {
//獲取字串id
int resourceId = titleIds[position];
//獲取字串
return context.getResources().getString(resourceId);
}
釋出效果和微信朋友圈類似,可以選擇圖片,和地理位置;地理位置使用高德地圖實現選擇,路徑規劃是呼叫系統中安裝的地圖,類似微信。
/**
* 搜尋該位置的poi,方便使用者選擇,也方便其他人找
* Point Of Interest,興趣點)
*/
private void searchPOI(LatLng data, String keyword) {
try {
Timber.d("searchPOI %s %s", data, keyword);
binding.progress.setVisibility(View.VISIBLE);
adapter.setNewInstance(new ArrayList<>());
// 第一個參數列示一個Latlng,第二參數列示範圍多少米,第三個參數列示是火系座標系還是GPS原生座標系
// val query = RegeocodeQuery(
// LatLonPoint(data.latitude, data.longitude)
// , 1000F, GeocodeSearch.AMAP
// )
//
// geocoderSearch.getFromLocationAsyn(query)
//keyWord表示搜尋字串,
//第二個參數列示POI搜尋型別,二者選填其一,選用POI搜尋型別時建議填寫型別程式碼,碼錶可以參考下方(而非文字)
//cityCode表示POI搜尋區域,可以是城市編碼也可以是城市名稱,也可以傳空字串,空字串代表全國在全國範圍內進行搜尋
PoiSearch.Query query = new PoiSearch.Query(keyword, "");
query.setPageSize(10); // 設定每頁最多返回多少條poiitem
query.setPageNum(0); //設定查詢頁碼
PoiSearch poiSearch = new PoiSearch(this, query);
poiSearch.setOnPoiSearchListener(this);
//設定周邊搜尋的中心點以及半徑
if (data != null) {
poiSearch.setBound(new PoiSearch.SearchBound(
new LatLonPoint(
data.latitude,
data.longitude
), 1000
));
}
poiSearch.searchPOIAsyn();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 使用高德地圖路徑規劃
*
* @param context
* @param slat 起點緯度
* @param slon 起點經度
* @param sname 起點名稱 可不填(0,0,null)
* @param dlat 終點緯度
* @param dlon 終點經度
* @param dname 終點名稱 必填
* 官方檔案:https://lbs.amap.com/api/amap-mobile/guide/android/route
*/
public static void openAmapRoute(
Context context,
double slat,
double slon,
String sname,
double dlat,
double dlon,
String dname
) {
StringBuilder builder = new StringBuilder("amapuri://route/plan?");
//第三方呼叫應用名稱
builder.append("sourceApplication=");
builder.append(context.getString(R.string.app_name));
//開始資訊
if (slat != 0.0) {
builder.append("&sname=").append(sname);
builder.append("&slat=").append(slat);
builder.append("&slon=").append(slon);
}
//結束資訊
builder.append("&dlat=").append(dlat)
.append("&dlon=").append(dlon)
.append("&dname=").append(dname)
.append("&dev=0")
.append("&t=0");
startActivity(context, Constant.PACKAGE_MAP_AMAP, builder.toString());
}
大部分真實專案中聊天都會選擇第三方商業級付費聊天服務,常用的有騰訊雲聊天,融雲聊天,網易雲聊天等,這裡選擇融雲聊天服務,使用步驟是先在伺服器端生成聊天Token,這裡是登入後返回,然後使用者端登入聊天伺服器,然後設定訊息監聽,傳送訊息等。
/**
* 連線聊天伺服器
*
* @param data
*/
private void connectChat(Session data) {
RongIMClient.connect(data.getChatToken(), new RongIMClient.ConnectCallback() {
/**
* 成功回撥
* @param userId 當前使用者 ID
*/
@Override
public void onSuccess(String userId) {
Timber.d("connect chat success %s", userId);
}
/**
* 錯誤回撥
* @param errorCode 錯誤碼
*/
@Override
public void onError(RongIMClient.ConnectionErrorCode errorCode) {
Timber.e("connect chat error %s", errorCode);
if (errorCode.equals(RongIMClient.ConnectionErrorCode.RC_CONN_TOKEN_INCORRECT)) {
//從 APP 服務獲取新 token,並重連
} else {
//無法連線 IM 伺服器,請根據相應的錯誤碼作出對應處理
}
//因為我們這個應用,不是類似微信那樣純聊天應用,所以聊天伺服器連線失敗,也讓進入應用
//真實專案中按照需求實現就行了
SuperToast.show(R.string.error_message_login);
}
/**
* 資料庫回撥.
* @param databaseOpenStatus 資料庫開啟狀態. DATABASE_OPEN_SUCCESS 資料庫開啟成功; DATABASE_OPEN_ERROR 資料庫開啟失敗
*/
@Override
public void onDatabaseOpened(RongIMClient.DatabaseOpenStatus databaseOpenStatus) {
}
});
}
chatClient.addOnReceiveMessageListener(new OnReceiveMessageWrapperListener() {
@Override
public void onReceivedMessage(Message message, ReceivedProfile profile) {
//該方法的呼叫不再主執行緒
Timber.e("chat onReceived %s", message);
if (EventBus.getDefault().hasSubscriberForEvent(NewMessageEvent.class)) {
//如果有監聽該事件,表示在聊天介面,或者對談介面
EventBus.getDefault().post(new NewMessageEvent(message));
} else {
handler.obtainMessage(0, message).sendToTarget();
}
//傳送訊息未讀數改變了通知
EventBus.getDefault().post(new MessageUnreadCountChangedEvent());
}
});
傳送圖片等其他訊息也是差不多。
private void sendTextMessage() {
String content = binding.input.getText().toString().trim();
if (StringUtils.isEmpty(content)) {
SuperToast.show(R.string.hint_enter_message);
return;
}
TextMessage textMessage = TextMessage.obtain(content);
RongIMClient.getInstance().sendMessage(Conversation.ConversationType.PRIVATE, targetId, textMessage, null, MessageUtil.createPushData(MessageUtil.getContent(textMessage), sp.getUserId()), new IRongCallback.ISendMessageCallback() {
@Override
public void onAttached(Message message) {
// 訊息成功存到本地資料庫的回撥
Timber.d("sendTextMessage onAttached %s", message);
}
@Override
public void onSuccess(Message message) {
// 訊息傳送成功的回撥
Timber.d("sendTextMessage success %s", message);
//清空輸入框
clearInput();
addMessage(message);
}
@Override
public void onError(Message message, RongIMClient.ErrorCode errorCode) {
// 訊息傳送失敗的回撥
Timber.e("sendTextMessage onError %s %s", message, errorCode);
}
});
}
先開啟SDK離線推播,還要分別去廠商那邊申請推播設定,這裡只實現了小米推播,其他的華為推播,OPPO推播等差不多;然後把推播,或者點選都統一代理到主介面,然後再處理。
private void postRun(Intent intent) {
String action = intent.getAction();
if (Constant.ACTION_CHAT.equals(action)) {
//本地顯示的訊息通知點選
//要跳轉到聊天介面
String id = intent.getStringExtra(Constant.ID);
startActivityExtraId(ChatActivity.class, id);
} else if (Constant.ACTION_PUSH.equals(action)) {
//聊天通知點選
String id = intent.getStringExtra(Constant.PUSH);
startActivityExtraId(ChatActivity.class, id);
}
}
學到這裡,大家不能說熟悉,那麼看到上面的介面,那麼大體要能實現出來。
//詳情
HtmlText.from(data.getDetail())
.setImageLoader(new HtmlImageLoader() {
@Override
public void loadImage(String url, final Callback callback) {
Glide.with(getHostActivity())
.asBitmap()
.load(url)
.into(new CustomTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
callback.onLoadComplete(resource);
}
@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
callback.onLoadFailed();
}
});
}
@Override
public Drawable getDefaultDrawable() {
return ContextCompat.getDrawable(getHostActivity(), R.drawable.placeholder);
}
@Override
public Drawable getErrorDrawable() {
return ContextCompat.getDrawable(getHostActivity(), R.drawable.placeholder_error);
}
@Override
public int getMaxWidth() {
return ScreenUtil.getScreenWith(getHostActivity());
}
@Override
public boolean fitWidth() {
return true;
}
})
.setOnTagClickListener(new OnTagClickListener() {
@Override
public void onImageClick(Context context, List<String> imageUrlList, int position) {
// image click
}
@Override
public void onLinkClick(Context context, String url) {
// link click
Timber.d("onLinkClick %s", url);
}
})
.into(binding.detail);
使用者端先整合微信,支付寶SDK,然後請求伺服器端獲取支付資訊,設定到SDK,最後就是處理支付結果。
/**
* 處理支付寶支付
*
* @param data
*/
private void processAlipay(String data) {
PayUtil.alipay(getHostActivity(), data);
}
/**
* 處理微信支付
*
* @param data
*/
private void processWechat(WechatPay data) {
//把伺服器端返回的引數
//設定到對應的欄位
PayReq request = new PayReq();
request.appId = data.getAppid();
request.partnerId = data.getPartnerid();
request.prepayId = data.getPrepayid();
request.nonceStr = data.getNoncestr();
request.timeStamp = data.getTimestamp();
request.packageValue = data.getPackageValue();
request.sign = data.getSign();
AppContext.getInstance().getWxapi().sendReq(request);
}
/**
* 支付寶支付狀態改變了
*
* @param event
*/
@Subscribe(threadMode = ThreadMode.MAIN)
public void onAlipayStatusChanged(AlipayStatusChangedEvent event) {
String resultStatus = event.getData().getResultStatus();
if ("9000".equals(resultStatus)) {
//本地支付成功
//不能依賴本地支付結果
//一定要以伺服器端為準
showLoading(R.string.hint_pay_wait);
//延時3秒
//因為支付寶回撥我們伺服器端可能有延遲
binding.primary.postDelayed(() -> {
checkPayStatus();
}, 3000);
} else if ("6001".equals(resultStatus)) {
//支付取消
SuperToast.show(R.string.error_pay_cancel);
} else {
//支付失敗
SuperToast.show(R.string.error_pay_failed);
}
}
這裡使用百度語音識別SDK,先整合,然後初始化,最後是監聽識別結果:
/**
* 百度語音識別事件監聽器
* <p>
* https://ai.baidu.com/ai-doc/SPEECH/4khq3iy52
*/
EventListener voiceRecognitionEventListener = new EventListener() {
/**
* 事件回撥
* @param name 回撥事件名稱
* @param params 回撥引數
* @param data 資料
* @param offset 開始位置
* @param length 長度
*/
@Override
public void onEvent(String name, String params, byte[] data, int offset, int length) {
String result = "name: " + name;
if (name.equals(SpeechConstant.CALLBACK_EVENT_ASR_READY)) {
// 引擎就緒,可以說話,一般在收到此事件後通過UI通知使用者可以說話了
setStopVoiceRecognition();
} else if (name.equals(SpeechConstant.CALLBACK_EVENT_ASR_PARTIAL)) {
// 一句話的臨時結果,最終結果及語意結果
if (params == null || params.isEmpty()) {
return;
}
// 識別相關的結果都在這裡
try {
JSONObject paramObject = new JSONObject(params);
//獲取第一個結果
JSONArray resultsRecognition = paramObject.getJSONArray("results_recognition");
String voiceRecognitionResult = resultsRecognition.getString(0);
//可以根據result_type是臨時結果,還是最終結果
binding.input.setText(voiceRecognitionResult);
result += voiceRecognitionResult;
} catch (JSONException e) {
e.printStackTrace();
}
} else if (name.equals(SpeechConstant.CALLBACK_EVENT_ASR_FINISH)) {
//一句話識別結束(可能含有錯誤資訊) 。最終識別的文字結果在ASR_PARTIAL事件中
if (params.contains("\"error\":0")) {
} else if (params.contains("\"error\":7")) {
SuperToast.show(R.string.voice_error_no_result);
} else {
//其他錯誤
SuperToast.show(getString(R.string.voice_error, params));
}
} else if (name.equals(SpeechConstant.CALLBACK_EVENT_ASR_EXIT)) {
//識別結束,資源釋放
setStartVoiceRecognition();
}
Timber.d("baidu voice recognition onEvent %s", result);
}
};
使用百度OCR從圖片中識別文字,主要是識別地址,類似順豐公眾號輸入地址時識別功能。
private void recognitionImage(String data) {
GeneralBasicParams param = new GeneralBasicParams();
param.setDetectDirection(true);
param.setImageFile(new File(data));
// 呼叫通用文字識別服務
OCR.getInstance(getApplicationContext()).recognizeGeneralBasic(param, new OnResultListener<GeneralResult>() {
/**
* 成功
* @param result
*/
@Override
public void onResult(GeneralResult result) {
StringBuilder builder = new StringBuilder();
for (WordSimple it : result.getWordList()) {
builder.append(it.getWords());
//每一項之間,新增空格,方便OCR失敗
builder.append(" ");
}
binding.input.setText(builder.toString());
}
/**
* 失敗
* @param error
*/
@Override
public void onError(OCRError error) {
SuperToast.show(getString(R.string.ocr_error, error.getMessage(), error.getErrorCode()));
}
});
}
還有一些功能,例如:快捷方式等就不在貼程式碼了。