首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 數據庫知識 >  >> SQL Server 資料庫 >> 關於SqlServer

Android 打造微信朋友圈:列表、多圖選擇、視頻錄製與播放完整實戰

     編輯:關於SqlServer

專案概覽

本專案完整實現一個「朋友圈」核心功能模組,包含:

  • 動態列表:下拉刷新、圖文/視頻混排、點贊評論交互
  • 多圖選擇器:掃描相冊、按資料夾分組、最多 9 張、預覽大圖
  • 視頻錄製:長按錄製、最短時長校驗、錄製完成自動跳轉預覽
  • 視頻播放:SurfaceView + MediaPlayer、點擊播放/暫停

技術棧:PullToRefreshListView、FinalBitmap 圖片載入、ContentResolver 掃描媒體庫、MediaRecorder/MediaPlayer 視頻處理。

核心模組代碼解析

1. 動態列表主 Activity (FriendsListActivity)

public class FriendsListActivity extends BaseActivity 
        implements OnRefreshListener<ListView>, PostListener {

    private InteractionAdapter mAdapter;
    private PullToRefreshListView mRefreshListView;
    private int mPage = 0;
    private boolean isRefreshing = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.friends_list);
        
        mAdapter = new InteractionAdapter(this);
        mAdapter.setListener(this);
        
        mRefreshListView = findViewById(R.id.friends_list);
        mRefreshListView.setOnRefreshListener(this);
        mRefreshListView.setAdapter(mAdapter);
        
        // 頂部發布按鈕:彈窗選擇 圖片/視頻/文字
        findViewById(R.id.btn_topost).setOnClickListener(v -> showPostDialog());
        
        loadData(0); // 首次載入
    }

    private void showPostDialog() {
        new AlertDialog.Builder(this)
            .setItems(new String[]{"圖片", "視頻", "文字"}, (dialog, which) -> {
                Intent intent = new Intent();
                if (which == 0) intent.setClass(this, CreatePostActivity.class);
                else intent.setClass(this, RecorderActivity.class); // 視頻/文字暫用同頁
                startActivity(intent);
            }).show();
    }

    private void loadData(int page) {
        if (isRefreshing) return;
        isRefreshing = true;
        mPage = page;
        FriendsApi.getFriendsList(this, new NetCallBack() {
            @Override public void friendslist(ArrayList<Interaction> list) {
                mAdapter.setInteractions(list);
                mRefreshListView.onRefreshComplete();
                isRefreshing = false;
            }
            @Override public void failed(String msg) { /* 處理失敗 */ }
        });
    }

    @Override public void onRefresh(PullToRefreshBase<ListView> v) { loadData(0); }
    @Override public void play(Interaction it) { /* 跳轉播放頁 */ }
}

關鍵點:介面回調模式 (PostListener) 解耦 Adapter 與 Activity;下拉刷新狀態鎖 isRefreshing 防重複請求。

2. 列表適配器 (InteractionAdapter) - ViewHolder 模式

public class InteractionAdapter extends BaseAdapter implements View.OnClickListener {
    // ... 欄位省略 ...

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
        if (convertView == null) {
            convertView = LayoutInflater.from(mContext).inflate(R.layout.friend_list_item, null);
            holder = new ViewHolder(convertView); // 綁定所有子 View
            convertView.setTag(holder);
        } else holder = (ViewHolder) convertView.getTag();

        Interaction it = interactions.get(position);
        // 1. 頭像
        mFinal.display(holder.avatar, it.avatar, config);
        // 2. 文字
        holder.title.setText(it.name);
        holder.content.setText(it.content);
        // 3. 圖片佈局 (0-3 張)
        if (it.images != null && !it.images.isEmpty()) {
            holder.imageLayout.setVisibility(View.VISIBLE);
            holder.imageLayout.setTag(it); // 整組圖片點擊事件
            // 根據數量顯示 1-3 張,用 FinalBitmap 載入
        } 
        // 4. 視頻佈局
        else if (it.videoPath != null) {
            holder.videoLayout.setVisibility(View.VISIBLE);
            holder.playBtn.setTag(it);
            holder.playBtn.setOnClickListener(this);
        }
        // 5. 更多操作
        holder.moreBtn.setTag(it);
        holder.moreBtn.setOnClickListener(this);
        return convertView;
    }

    @Override public void onClick(View v) {
        Interaction it = (Interaction) v.getTag();
        if (v.getId() == R.id.image_layout) { /* 跳轉圖片瀏覽器 */ }
        else if (v.getId() == R.id.btn_play_result) { /* 觸發 play 回調 */ }
        else if (v.getId() == R.id.more) { /* 觸發 show 回調 */ }
    }
}

亮點:單一 getView 內處理「純文字、多圖、視頻」三種 Item 型態,透過 Visibility.GONE/VISIBLE 切換佈局,避免多型 Item 的 getViewTypeCount 複雜度;圖片載入統一交給 FinalBitmap 管理快取與動畫。

3. 多圖選擇器 (MultipleActivity) - ContentResolver 掃描優化

// 子執行緒掃描媒體庫
new Thread(() -> {
    Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
    String[] proj = {MediaStore.Images.Media.DATA};
    String sel = MediaStore.Images.Media.MIME_TYPE + "=? OR " + MediaStore.Images.Media.MIME_TYPE + "=?";
    String[] args = {"image/jpeg", "image/png"};
    
    Cursor cursor = getContentResolver().query(uri, proj, sel, args, 
                            MediaStore.Images.Media.DATE_MODIFIED + " DESC");
    
    HashSet<String> dirSet = new HashSet<>(); // 去重資料夾
    List<ImageFloder> folders = new ArrayList<>();
    
    while (cursor.moveToNext()) {
        String path = cursor.getString(0);
        File parent = new File(path).getParentFile();
        if (parent == null || !dirSet.add(parent.getAbsolutePath())) continue;
        
        ImageFloder floder = new ImageFloder();
        floder.setDir(parent.getAbsolutePath());
        floder.setFirstImagePath(path);
        floder.setCount(countImagesInDir(parent)); // FilenameFilter 過濾 jpg/png
        folders.add(floder);
        
        if (floder.getCount() > maxCount) { maxCount = floder.getCount(); maxDir = parent; }
    }
    cursor.close();
    dirSet = null; // 釋放記憶體
    mHandler.sendEmptyMessage(MSG_SCAN_DONE);
}).start();

優化細節:

  • HashSet 防止同資料夾重複掃描,大幅降低記憶體佔用。
  • 只查詢 _DATA 欄位,減少 Cursor 開銷。
  • 資料夾與圖片路徑分離儲存,Adapter 組合完整路徑,避免重複字串物件。

4. 視頻錄製 (RecorderActivity + MovieRecorderView)

// 錄製按鈕觸摸監聽
mShootBtn.setOnTouchListener((v, event) -> {
    if (event.getAction() == MotionEvent.ACTION_DOWN) {
        mRecorderView.record(() -> handler.sendEmptyMessage(MSG_RECORD_DONE));
    } else if (event.getAction() == MotionEvent.ACTION_UP) {
        if (mRecorderView.getTimeCount() > 1) { // 最短 1 秒
            handler.sendEmptyMessage(MSG_RECORD_DONE);
        } else {
            mRecorderView.getRecordFile().delete(); // 太短刪除
            mRecorderView.stop();
            Toast.makeText(this, "時間太短", Toast.LENGTH_SHORT).show();
        }
    }
    return true;
});

生命週期管理:onPause/onSaveInstanceState 中呼叫 mRecorderView.stop() 釋放 MediaRecorder,防止資源洩漏導致後續錄製失敗。

5. 視頻播放 (RecorderPlayActivity)

public class RecorderPlayActivity extends Activity 
        implements SurfaceHolder.Callback, View.OnClickListener {

    private MediaPlayer player;
    private SurfaceView surfaceView;
    private String path;

    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.recorder_play);
        path = getIntent().getStringExtra("path");
        
        surfaceView = findViewById(R.id.play_view);
        surfaceView.getHolder().addCallback(this);
        surfaceView.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
        findViewById(R.id.ib_play).setOnClickListener(this);
    }

    @Override public void surfaceCreated(SurfaceHolder holder) {
        player = new MediaPlayer();
        player.setDisplay(holder);
        try { player.setDataSource(path); player.prepare(); } 
        catch (IOException e) { e.printStackTrace(); }
    }

    @Override public void onClick(View v) {
        if (v.getId() == R.id.ib_play) { player.start(); v.setVisibility(View.GONE); }
        else if (v.getId() == R.id.play_view) { player.pause(); findViewById(R.id.ib_play).setVisibility(View.VISIBLE); }
    }

    @Override protected void onDestroy() {
        if (player != null) { player.stop(); player.release(); }
        super.onDestroy();
    }
}

注意:SurfaceHolder.Callback 確保 Surface 準備好再 prepare();onDestroy 必須 release() 釋放硬體解碼器資源。

專案結構與資源

朋友圈列表截圖

發布選單截圖

多圖選擇器截圖

總結與擴展建議

  1. 架構升級:採用 MVVM + LiveData + Paging 3 替代 Handler + Adapter,支援無限滾動與配置變更存活。
  2. 圖片選擇器:改用 PhotoPicker (Android 13+) 或 ActivityResultContracts.PickMultipleVisualMedia 簡化權限與掃描邏輯。
  3. 視頻壓縮:引入 MediaCodec 或 FFmpeg 進行硬體編碼壓縮,控制上傳體積。
  4. 離線快取:Room 儲存動態草稿,WorkManager 處理斷點續傳上傳。

完整代碼已上傳 GitHub (連結略),歡迎 Star、Fork 交流改進!

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved