<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Android相冊開發(fā)實戰(zhàn)!(附源碼)

          共 26735字,需瀏覽 54分鐘

           ·

          2023-09-19 16:22

             
             
           安卓進階漲薪訓練營,讓一部分人先進大廠


          大家好,我是皇叔,最近開了安卓進階漲薪訓練營第二期,可以幫助大家突破技術(shù)&職場瓶頸,從而度過難關,進入心儀的公司。


          詳情見文章:皇叔安卓訓練營第二期開啟啦!

          作者:呼嘯長風
          https://juejin.cn/user/2999123450531982

          我之前發(fā)布了個圖片加載框架,在JCenter關閉后,“閉關修煉”,想著改好了出個2.0版本。后來覺得僅增加功能和改進實現(xiàn)不夠,得補充一下用例。相冊列表的加載就是很好的用例,然后在Github找了一圈,沒有找到滿意的,有的甚至好幾年沒維護了,于是就自己寫了一個。


          代碼鏈接:

          https://github.com/BillyWei01/EasyAlbum


          相比于圖片加載,相冊加載在Github上要多很多。其原因大概是圖片加載的input/output比較規(guī)范,不涉及UI布局;而相冊則不然,幾乎每個APP都會有自己獨特的需求,有自己的UI風格。因此,相冊庫很難做到通用于大部分APP。我所實現(xiàn)的這個也一樣,并非以實現(xiàn)通用的相冊組件為目的,而是作為一個樣例,以供參考。

          需求描述

          網(wǎng)上不少相冊的開源庫,都是照微信相冊來搭的界面,我也是跟著這么做吧,要是說涉及侵權(quán)什么的,那些前輩應該先比我收到通知……


          主要是自己也不會UI設計,不找個參照對象怕實現(xiàn)的太難看。話說回來,要是真的涉及侵權(quán),請聯(lián)系我處理。相冊所要實現(xiàn)的功能,概括來說,就是顯示相冊列表,點擊縮略圖選中,點擊完成結(jié)束選擇,返回選擇結(jié)果。需求細節(jié),包括但不限于以下列表:


            • 實現(xiàn)目錄列表,相冊列表,預覽頁面;
            • 支持單選/多選;
            • 支持顯示選擇順序和限定選擇數(shù)量;
            • 支持自定義篩選條件;
            • 支持自定義目錄排序;
            • 支持“原圖”選項;
            • 支持再次進入相冊時傳入已經(jīng)選中的圖片/視頻;
            • 支持切換出APP外拍照或刪除照片后,回到相冊時自動刷新;



          效果如圖:

          API設計

          由于不同的頁面可能需求不一樣,所以可以將需求參數(shù)封裝到“Request”中;對于通用的選項,以及相冊組件的全局配置,可以更封裝到“Config"中。而Request/Config最好是用鏈式API去設置參數(shù),鏈式API尤其適合參數(shù)是“可選項”的場景。

          全局設置

              
              
          EasyAlbum.config()
              .setImageLoader(GlideImageLoader)
              .setDefaultFolderComparator { o1, o2 -> o1.name.compareTo(o2.name)}

          GlideImageLoader是相冊組件定義的ImageLoader接口的實現(xiàn)類。
              
              
          public interface ImageLoader {
              void loadPreview(MediaData data, ImageView imageView, boolean asBitmap);

              void loadThumbnail(MediaData data, ImageView imageView, boolean asBitmap);
          }

          不同的APP使用的圖片加載框架不一樣,所以相冊組件最好不要強依賴圖片加載框架,而是暴露接口給調(diào)用者。

          當然,對于整個APP而言,不建議定義這樣的ImageLoader類,因為APP使用圖片加載的地方很多,定義這樣的類,要么需要重載很多方法,要么就是參數(shù)列表很長,也就喪失鏈式API的優(yōu)點了。關于目錄排序,EasyAlbum中定義的默認排序是按照更新時間(取最新的圖片的更新時間)排序。上面代碼舉例的是按目錄名排序。如果需要某個目錄排在列表前面,可以這樣定義(以“Camera”為例)。

              
              
          private val priorityFolderComparator = Comparator<Folder> { o1, o2 ->
              val priorityFolder = "Camera"
              if (o1.name == priorityFolder) -1
              else if (o2.name == priorityFolder) 1
              else o1.name.compareTo(o2.name)
          }

          出個思考題

          如果需要“優(yōu)先排序”的不只一個目錄,比如希望“Camera"第一優(yōu)先,"Screenshots"第二優(yōu)先,“Pictures"第三優(yōu)先……改如何定義Comparator?

          啟動相冊

          EasyAlbum啟動相冊以from起頭,以start結(jié)束。
              
              
          EasyAlbum.from(this)
              .setFilter(TestMediaFilter(option))
              .setSelectedLimit(selectLimit)
              .setOverLimitCallback(overLimitCallback)
              .setSelectedList(mediaAdapter?.getData())
              .setAllString(option.text)
              .enableOriginal()
              .start { result ->
                  mediaAdapter?.setData(result.selectedList)
              }

          具體到實現(xiàn),就是from返回Request,Request的start方法啟動相冊頁(AlbumActivity)。
              
              
          public class EasyAlbum {
              public static AlbumRequest from(@NonNull Context context) {
                  return new AlbumRequest(context);
              }
          }

              
              
          public final class AlbumRequest {
              private WeakReference<Context> contextRef;

              AlbumRequest(Context context) {
                  this.contextRef = new WeakReference<>(context);
              }

              // ...其他參數(shù)..

              public void start(ResultCallback callback{
                  Session.init(this, callback, selectedList);
                  if (contextRef != null) {
                      Context context = contextRef.get();
                      if (context != null) {
                          context.startActivity(new Intent(context, AlbumActivity.class));
                      }
                      contextRef = null;
                  }
              }
          }

          啟動AlbumActivity,就涉及傳參和結(jié)果返回。有兩種思路:

          1. 通過intent傳參數(shù)到AlbumActivity, 用startActivityForResult啟動,通過onActivityResult接收。
          2. 通過靜態(tài)變量傳遞參數(shù),通過Callback回調(diào)結(jié)果。

          第一種方法,需要所有的參數(shù)都能放入Intent,基礎數(shù)據(jù)可以傳,自定義數(shù)據(jù)類可以實現(xiàn)Parcelable,但那對于接口的實現(xiàn),就沒辦法放 intent 了,到頭來還是要走靜態(tài)變量。因此,干脆就都走靜態(tài)變量傳遞好了。

          這個方案可行的前提是, AlbumActivity是封閉的,不會在跳轉(zhuǎn)其他Activity。在這個前提下,App不會同一個時刻打開多個AlbumActivity,不需要擔心共享變量相互干擾的情況。然后就是在Activity結(jié)束時,做好清理工作。可以將“啟動相冊-選擇圖片-結(jié)束相冊”抽象為一次“Session”,  在相冊結(jié)束時,執(zhí)行一下clear操作。

              
              
          final class Session {
              static AlbumRequest request;
              static AlbumResult result;
              private static ResultCallback resultCallback;

              static void init(AlbumRequest req, ResultCallback callback, List<MediaData> selectedList) {
                  request = req;
                  resultCallback = callback;
                  result = new AlbumResult();
                  if (selectedList != null) {
                      result.selectedList.addAll(selectedList);
                  }
              }

              static void clear() {
                  if (request != null) {
                      request.clear();
                      request = null;
                      resultCallback = null;
                      result = null;
                  }
              }
          }

          媒體文件加載

          媒體文件加載似乎很簡單,就調(diào)ContentResolver query一下的事,但要做到盡量完備,需要考慮的細節(jié)還是不少的。

          MediaStore API

          查詢媒體數(shù)據(jù)庫,需走ContentResolver的qurey方法:
              
              
          public final Cursor query( 
            Uri uri,
            String[] projection, 
            String selection, 
            String[] selectionArgs, 
            String sortOrder,
            CancellationSignal cancellationSignal) {
          }

          媒體數(shù)據(jù)庫記錄了各種媒體類型,要過濾其中的“圖片”和“視頻”,有兩種方法:

          1、用SDK定義好的MediaStore.Video和MediaStore.Images的Uri。

              
              
          MediaStore.Video.Media.EXTERNAL_CONTENT_URI
          MediaStore.Images.Media.EXTERNAL_CONTENT_URI

          2、直接讀取"content://external",通過MEDIA_TYPE字段過濾。
              
              
          private static final Uri CONTENT_URI = MediaStore.Files.getContentUri("external");

          private static final String TYPE_SELECTION = "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "="
                  + MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO
                  + " OR " + MediaStore.Files.FileColumns.MEDIA_TYPE + "="
                  + MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE
                  + ")";

          如果需要同時讀取圖片和視頻,第2種方法更省事一些。至于查詢的字段,視需求而定。以下是比較常見的字段:
              
              
          private static final String[] PROJECTIONS = new String[]{
                  MediaStore.MediaColumns._ID,
                  MediaStore.MediaColumns.DATA,
                  MediaStore.Files.FileColumns.MEDIA_TYPE,
                  MediaStore.MediaColumns.DATE_MODIFIED,
                  MediaStore.MediaColumns.MIME_TYPE,
                  MediaStore.Video.Media.DURATION,
                  MediaStore.MediaColumns.SIZE,
                  MediaStore.MediaColumns.WIDTH,
                  MediaStore.MediaColumns.HEIGHT,
                  MediaStore.Images.Media.ORIENTATION
          };

          DURATION,SIZE,WIDTH,HEIGHT,ORIENTATION等字段有可能是無效的(0或者null),如果是無效的,可以去從文件本身獲取,但讀文件比較耗時,所以可以先嘗試從MediaStore讀取,畢竟是都訪問到這條記錄了,從空間局部原理來說,讀取這些字段是順便的事情,代價要比另外讀文件本身低很多。當然,如果確實不需要這些信息,可以直接不讀取。

          數(shù)據(jù)包裝

          數(shù)據(jù)查詢出來,需要定義Entity來包裝數(shù)據(jù)。
              
              
          public final class MediaData implements Comparable<MediaData{
              private static final String BASE_VIDEO_URI = "content://media/external/video/media/";
              private static final String BASE_IMAGE_URI = "content://media/external/images/media/";

              static final byte ROTATE_UNKNOWN = -1;
              static final byte ROTATE_NO = 0;
              static final byte ROTATE_YES = 1;

              public final boolean isVideo;
              public final int mediaId;
              public final String parent;
              public final String name;
              public final long modifiedTime; // in seconds
              public String mime;

              long fileSize;
              int duration;
              int width;
              int height;
              byte rotate = ROTATE_UNKNOWN;

              public String getPath() {
                  return parent + name;
              }

              public Uri getUri() {
                  String baseUri = isVideo ? BASE_VIDEO_URI : BASE_IMAGE_URI;
                  return Uri.parse(baseUri + mediaId);
              }

              public int getRealWidth() {
                  if (rotate == ROTATE_UNKNOWN || width == 0 || height == 0) {
                      fillData();
                  }
                  return rotate != ROTATE_YES ? width : height;
              }

              public int getRealHeight() {
                  if (rotate == ROTATE_UNKNOWN || width == 0 || height == 0) {
                      fillData();
                  }
                  return rotate != ROTATE_YES ? height : width;
              }

              // ......
          }

          數(shù)據(jù)共享

          字段的定義中,沒有直接定義path字段,而是定義了parent和name,因為圖片/視頻文件可能有成千上萬個,但是目錄大概率不會超過3位數(shù),所以,我們可以通過復用parent來節(jié)約內(nèi)存。同理,mime也可以復用。截取部分查詢的代碼:
              
              
          int count = cursor.getCount();
          List<MediaData>  list = new ArrayList<>(count);
          while (cursor.moveToNext()) {
              String path = cursor.getString(IDX_DATA);
              String parent = parentPool.getOrAdd(Utils.getParentPath(path));
              String name = Utils.getFileName(path);
              String mime = mimePool.getOrAdd(cursor.getString(IDX_MIME_TYPE));
               // ......
          }

          復用字符串,可以用HashMap來做,我這邊是仿照HashMap寫了一個專用的類來實現(xiàn)。getOrAdd方法:傳入一個字符串,如果容器中已經(jīng)有這個字符串,返回容器保存的字符串,否則,保存當前字符串并返回。如此,所有的MediaData共用相同parent和mime字符串對象。

          處理無效數(shù)據(jù)

          前面提到,從MediaStore讀取的數(shù)據(jù),有部分是無效的。這些可能無效的字段不要直接public, 而是提供get方法,并在返回之前檢查數(shù)據(jù)的有效性,如果數(shù)據(jù)無效則讀文件獲取數(shù)據(jù)。當然,讀文件是耗時操作,雖然一般情況下時間是可控的,但是最好還是放IO線程去訪問比較保險。也有比較折中的做法。

          數(shù)據(jù)只是用作參考,有的話更好,沒有也沒關系。

          如果是這樣的話,提供不做檢查直接返回數(shù)據(jù)的方法:
              
              
              public int getWidth() {
                  return rotate != ROTATE_YES ? width : height;
              }

              public int getHeight() {
                  return rotate != ROTATE_YES ? height : width;
              }

          數(shù)據(jù)比較重要,但也不至于沒有就不行。

          這種case,當數(shù)據(jù)無效時,可以先嘗試讀取,但是加個timeout,在規(guī)定時間內(nèi)沒有完成讀取則直接返回。
              
              
              public int getDuration() {
                  if (isVideo && duration == 0) {
                      checkData();
                  }
                  return duration;
              }

              void checkData() {
                  if (!hadFillData) {
                      FutureTask<Boolean> future = new FutureTask<>(this::fillData);
                      try {
                          // Limit the time for filling extra info, in case of ANR.
                          AlbumConfig.getExecutor().execute(future);
                          future.get(300, TimeUnit.MILLISECONDS);
                      } catch (Throwable ignore) {
                      }
                  }
              }

          數(shù)據(jù)加載

          數(shù)據(jù)加載部分是最影響相冊體驗的因素之一。等待時間、數(shù)據(jù)刷新、數(shù)據(jù)有效性等都會影響相冊的交互。

          緩存MediaData
          媒體庫查詢是一個綜合IO讀取和CPU密集計算的操作,文件少的時候還好,一旦文件比較多,耗時幾秒鐘也是有的。如果用戶每次打開相冊都要等幾秒鐘才刷出數(shù)據(jù),那體驗就太糟糕了。加個MediaData的緩存,再次進入相冊時,就不需要再次讀所有字段了,只需讀取MediaStore的ID字段,然后結(jié)合緩存,做下Diff, 已刪除的移除出緩存,新增的根據(jù)ID檢索其記錄,創(chuàng)建MediaData添加到緩存。再次進入相冊,即使有增刪也不會太多。緩存MediaData的好處不僅僅是加速再次查詢MediaStore,還可以減少對象的創(chuàng)建,不需要每次查詢都重新創(chuàng)建MediaData對象;另外,前面也提到,MediaData部分字段有的是無效的,在無效時需要讀取原文件獲取,緩存MediaData可免去再次讀文件獲取數(shù)據(jù)的時間(如果對象是讀取MediaStore重新創(chuàng)建的,就又回到無效的狀態(tài)了)。

          還有就是,有緩存的話,就可以做預加載了。當然這個得看APP是否有這個需求,如果APP是媒體相關的,大概率要訪問相冊的,可以考慮預加載。做緩存的代價就是要占用些內(nèi)存,這也是前面MediaData為什么復用parent和mime的原因。

          緩存是空間換時間,復用對象是時間換空間,總體而言這個對沖是賺的,因為讀取IO更耗時。另外,如果有必要,可以提供clearCache接口,在適當?shù)臅r機清空緩存。

          組裝結(jié)果

          相冊的UI層所需要的是,根據(jù)Request的查詢條件過濾后的MediaData,以目錄為分組,按更新時間降序排列的數(shù)據(jù)。緩存的MediaData并非查詢的終點,但卻提供了一個好的起點。在有緩存好的MediaData列表的前提下,可直接根據(jù)MediaData列表做過濾,排序和分組,而不需要每次都將過濾條件拼接SQL到數(shù)據(jù)庫中查詢,而且相比于拼接SQL,在上層直接根據(jù)MediaData過濾要更加靈活。

          下面是EasyAlbum基于MediaData緩存的查詢:
              
              
          private static List<Folder> makeResult(AlbumRequest request) {
                  AlbumRequest.MediaFilter filter = request.filter;
                  ArrayList<MediaData> totalList = new ArrayList<>(mediaCache.size());

                  if (filter == null) {
                      totalList.addAll(mediaCache.values());
                  } else {
                        // 根據(jù)filter過濾MediaData
                      for (MediaData item : mediaCache.values()) {
                          if (filter.accept(item)) {
                              totalList.add(item);
                          }
                      }
                  }

                  // 先對所有MediaData排序,后面分組后就不需要繼續(xù)在分組內(nèi)排序了
                  // 因為分組時是按順序放到分組列表的。
                  Collections.sort(totalList);

                  Map<String, ArrayList<MediaData>> groupMap = new HashMap<>();
                  for (MediaData item : totalList) {
                      String parent = item.parent;
                      ArrayList<MediaData> subList = groupMap.get(parent);
                      if (subList == null) {
                          subList = new ArrayList<>();
                          groupMap.put(parent, subList);
                      }
                      subList.add(item);
                  }

                  final List<Folder> result = new ArrayList<>(groupMap.size() + 1);
                  for (Map.Entry<String, ArrayList<MediaData>> entry : groupMap.entrySet()) {
                      String folderName = Utils.getFileName(entry.getKey());
                      result.add(new Folder(folderName, entry.getValue()));
                  }

                  // 對目錄排序
                  Collections.sort(result, request.folderComparator);

                  // 最后,總列表放在最前
                  result.add(0new Folder(request.getAllString(), totalList));
                  return result;
              }

          MediaFilter的定義如下:
              
              
          public interface MediaFilter {
              boolean accept(MediaData media);

              // To identify the filter
              String tag();
          }

          基于MediaData緩存列表的查詢雖然比基于數(shù)據(jù)庫的查詢快不少,但是當文件很多時,也還是要花一些時間的。所以我們可以再加一個緩存,緩存最終結(jié)果。

          再加一個結(jié)果緩存,只是增加了些容器,容器指向的對象(MediaData)是之前MediaData緩存列表所引用的對象,所以代價還好。再次進入相冊時,可以先直接取結(jié)果顯示,然后再去檢查MediaStore相對于緩存有沒有變更,有則刷新緩存和UI,否則直接返回。APP可能有多個地方需要相冊,不同地方查詢條件可能不一樣,所以MediaFilter定義了tag接口,用來區(qū)分不同的查詢。

          加載流程

          流程圖如下。注意,下圖的“結(jié)果”是提供給相冊頁面顯示的數(shù)據(jù),并非相冊返回給調(diào)用者的“已選中的媒體”。


          做了兩層緩存,加載流程是復雜一些。但好處也是顯而易見的,增加了結(jié)果緩存之后,再次啟動相冊就基本是“秒開”了。查詢過程是在后臺線程中執(zhí)行的,結(jié)果通過handler發(fā)送給AlbumActivity。

          圖中還有一些小處理沒畫出來。比如,首次加載,在發(fā)送結(jié)果給相冊界面之后,還會繼續(xù)執(zhí)行一個“檢查文件是否已刪除”的操作。針對的是這么一種情況,MediaStore中的記錄,DATA字段所對應的文件不存在。我自己的設備上是沒有出現(xiàn)過這種case, 我也是聽前輩講的,或許他們遇到過。如果確實有設備存在這樣的情況,的確應該檢查一下,否則相冊滑動到這些“文件不存在”的記錄時,會只看到一片黑,稍微影響體驗。但由于我自己沒有具體考證,所以在EasyAblum的全局配置中留了option, 可以設置不執(zhí)行。關于這點大家按具體情況自行評估。加載流程一般在進入相冊頁時啟動。

          考慮到用戶在瀏覽相冊時,有時候可能會切換出去拍照或者刪除照片,可在onResume的時候也啟動一下加載流程,檢查是否有媒體文件增刪。

          相冊列表

          媒體縮略圖

          Android系統(tǒng)對相冊文件提供了獲取縮略圖的API,通過該API獲取圖片要比直接讀取媒體文件本身要快很多。一些圖片加載框架中有實現(xiàn)相關邏輯,比如Glide的實現(xiàn)了MediaStoreImageThumbLoader和MediaStoreVideoThumbLoader,但是所用API比較舊,在我的設備(Android 10)上已經(jīng)不生效了。如果使用Glide的朋友可以自行實現(xiàn)ModelLoader和ResourceDecoder來處理。EasyAlbum的Demo中有實現(xiàn),感興趣的朋友可以參考一下。

          列表布局

          相冊列表通常是方格布局,如果RecycleView布局,最好能讓每一列都等寬。下面這個ItemDecoration的實現(xiàn)是其中一種方法:

              
              
          public class GridItemDecoration extends RecyclerView.ItemDecoration {
              private final int n; // 列的數(shù)量
              private final int space; // 列與列之間的間隔
              private final int part; // 每一列應該分攤多少間隔

              public GridItemDecoration(int n, int space) {
                  this.n = n;
                  this.space = space;
                   // 總間隔:space * (n - 1) ,等分n份
                  part = space * (n - 1) / n;
              }

              @Override
              public void getItemOffsets(
                      @NonNull Rect outRect,
                      @NonNull View view,
                      @NonNull RecyclerView parent,
                      @NonNull RecyclerView.State state)
           
          {
                  int position = parent.getChildLayoutPosition(view);
                  int i = position % n;
                  // 第i列(0開始)的左邊部分的間隔的計算公式:space * i / n
                  outRect.left = Math.round(part * i / (float) (n - 1));
                  outRect.right = part - outRect.left;
                  outRect.top = 0;
                  outRect.bottom = space;
              }
          }

          其原理就是將所有space加起來,等分為n份,每個item分攤1份。其中第i列(index從0開始)的左邊部分的間隔的計算公式為:space * i / n 。 比方說colomn = 4,那么就有3個space。如果每個space=4px,則每個item分攤4 * (4-1)/ 4 = 3px。


          • 第1個item的left=0px, right = 3px;
          • 第2個item的left=1px, right = 2px;
          • 第3個item的left=2px, right =1px;
          • 第4個item的left=3px, right =0px。

          于是,每個間隔看起來都是4px,且每個item的left+right都是相等的,所以留給view的寬度是相等的。效果如下圖:


          有的地方是這么去分配left和right的:

              
              
          outRect.left = column == 0 ? 0 : space / 2;
          outRect.right = column == (n - 1) ? 0 : space / 2;

          這樣能讓每個間隔的大小相等,但是view本身的寬度就不相等了。效果如下圖:


          左右兩個item分別比中間的item多了2px。這2px看上去不多,但是可能會導致列表變更(增刪)時,圖片框架的緩存失效。例如如果刪除了最接近的一張照片,原第2-4列會移動到1-3列,原第1列會移動到第4列。于是第2列的寬度從266變?yōu)?88,第4列的寬度從288變?yōu)?66,而圖片加載框架的target寬高是緩存key的計算要素之一,寬度變了,就不能命中之前的緩存了。

          后序

          相冊的實現(xiàn)可簡單可復雜,我見過的最簡單的實現(xiàn)是直接在主線程查詢媒體數(shù)據(jù)庫的……本文從各個方面分享了一些相冊實現(xiàn)的經(jīng)驗,尤其是相冊加載部分。目前這個時代,手機存幾千上萬張圖片是很常見的,優(yōu)化好相冊的加載,能提升不少用戶體驗。項目已發(fā)布到Github和Maven Central。

          Github地址如下:
          https://github.com/BillyWei01/EasyAlbum

          引用方式:
              
              
          implementation 'io.github.billywei01:easyalbum:1.0.6'





          為了防止失聯(lián),歡迎關注我防備的小號


           


                         微信改了推送機制,真愛請星標本公號??

          瀏覽 2457
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产精品久久久久久爽爽爽麻豆色哟哟 | 五月天Av成人在线播放 | 波多野在线一区 | 成人做爱在线观看 | 一区二三区三区四区五区视频 |