mediaList; // 九宫格数据(图片和视频)
215 |
216 | // 以下省略构造方法和Getter、Setter
217 | }
218 | ```
219 |
220 | 其实软件工程规范的开发流程就是这样的,先有设计图,然后再针对设计图进行设计数据结构。
221 |
222 | 什么时候你能达到看着设计图就能想到对应的JavaBean结构,那么我想,一般的App开发任务你就可以胜任了。
223 |
224 | JavaBean就相当于一个接收数据的容器,现在容器有了,我们需要去获取数据了。
225 |
226 | ### 1、QZoneActivity.java(获取网络数据)
227 |
228 | Activity和Fragment是Android中两类UI控制器,MVC架构中的C层,MVP架构中的P层。
229 |
230 | 获取后台数据的方法:Okhttp请求网络 + Json解析,详情不是本次的重点,我就不多说了,你都会了。 :)
231 |
232 | 因为我没有后台,所以数据是我手动设置。
233 |
234 |
235 |
236 | 👈点击展开 | 这些内容可以略过,故而折叠起来了
237 | // 自定义的测试数据(假装这是网络请求并解析后的数据)
238 | private void loadMyTestDate(){
239 | // 先构造MyMedia
240 | String imgUrl1 = "http://i2.tiimg.com/702441/6e3d61b352409f34.png";
241 | String imgUrl2 = "http://i2.tiimg.com/702441/ca8092e87a2f2b30.jpg";
242 | String imgUrl3 = "http://i2.tiimg.com/702441/081b443af609c94c.png";
243 | String videoUrl = "http://jzvd.nathen.cn/c6e3dc12a1154626b3476d9bf3bd7266/6b56c5f0dc31428083757a45764763b0-5287d2089db37e62345123a1be272f8b.mp4";
244 | MyMedia myMedia1 = new MyMedia(imgUrl1,videoUrl);
245 | MyMedia myMedia2 = new MyMedia(imgUrl2);
246 | MyMedia myMedia3 = new MyMedia(imgUrl3);
247 | // 再构造mediaList
248 | ArrayList mediaList = new ArrayList<>();
249 | for (int i = 0; i < 3; i++) { // 加入9张图片
250 | mediaList.add(myMedia1);
251 | mediaList.add(myMedia2);
252 | mediaList.add(myMedia3);
253 | }
254 | Location location = new Location();
255 | location.setAddress("Test Address");
256 | // 最后构造RecyclerViewItem
257 | final RecyclerViewItem RecyclerViewItem1 = new RecyclerViewItem(mediaList, "河北经贸大学...",
258 | "2019-11-02","10080", "自强社", location, imgUrl1);
259 | final RecyclerViewItem RecyclerViewItem2 = new RecyclerViewItem(mediaList, "河北经贸大学...",
260 | "2019-11-02","10080", "信息技术学院", location, imgUrl2);
261 | final RecyclerViewItem RecyclerViewItem3 = new RecyclerViewItem(mediaList, "河北经贸大学...",
262 | "2019-11-02","10080", "雷雨话剧社", location, imgUrl3);
263 | RecyclerViewItemList.add(RecyclerViewItem1);
264 | RecyclerViewItemList.add(RecyclerViewItem2);
265 | RecyclerViewItemList.add(RecyclerViewItem3);
266 | }
267 |
268 |
269 |
270 | 通过QZoneActivity我们可以拿到所有需要展示的数据,但这仅仅是数据,我们需要把数据和具体的UI控件进行绑定。
271 |
272 | 如果说是单纯的一个ImageView进行数据绑定,那么完全可以就在 QZoneActivity 中进行,比如我们通过Glide框架,很方便的**一行代码**就能绑定图片url到ImageView中
273 |
274 | #### 1.1 Glide框架的简单使用
275 |
276 | ```java
277 | // 使用前记得先添加dependency
278 | Context context = getApplicationContext();
279 | String url = "图片url";
280 | ImageView imageView = findViewById(R.id.iv);
281 |
282 | Glide.with(context).load(url).into(imageView); // 一行代码指的是这一行
283 | ```
284 |
285 | 但是,我们要呈现的界面可不仅仅只有一张图片。
286 |
287 | 我们要呈现的是一个可以滑动的RecyclerView或者ListView,因为这两个控件一般需要把很多UI控件和很多数据进行绑定,如果全都写在一个类里面,一是类显得过于臃肿,不好理解;二是代码太密集,不便快速定位问题;三是代码耦合度太高,不符合优秀的编程思想(这也是产生前面两个问题的终极原因)。
288 |
289 | 所以,我们需要把数据获取和数据绑定进行分离,这就用到了适配器模式。
290 |
291 | #### 1.2 适配器思想
292 |
293 | 原先代码逻辑是:Activity从XML布局文件中获取UI控件对象,同时Activity也从网络上获取数据,然后在Activity中将二者进行绑定。
294 |
295 |
296 |
297 | 引入适配器之后:Activity只负责获取网络数据,拿到之后传递给适配器。适配器再从XML布局文件中拿到UI控件,将二者进行绑定。通过引入适配器,数据获取与数据绑定就解耦合了。
298 |
299 |
300 |
301 | 原先的Activity既需要获取数据,又需要绑定数据。适配器的出现帮他分离了绑定数据的代码,让Activity只做好获取数据一件事就行,拿到数据之后,传给适配器就行。这就是所谓的解耦合。
302 |
303 | 这样更适合互联网公司多人协同开发,一些人只管写布局文件,一些人只管写适配器,一些人只管写Activity。如果配合得当,效率肯定是比原先的快,这样就能更快的上线App产品,更快的抢占市场,更快的获取利润,更快的成为人生赢家,更快的迎娶白富美,更快的为社会主义现代化建设做贡献,更快的实现共产主义,更快的......
304 |
305 | (喂ヽ(●-`Д´-)ノ,醒醒,醒醒......大郎,该喝药了!)
306 |
307 | ### 2、RecyclerViewItemAdapter.java
308 |
309 | 刚才我们说的是QZoneActivity.java中的代码,它获取到网络数据之后(其实是我手写的测试数据),就将数据以List的方式传递给了适配器,这个适配器就是RecyclerViewItemAdapter。先假设适配器已经完成了数据绑定的工作(不去看里面具体的代码),一旦数据绑定完成,我们就可以将适配器设置给RecyclerView / ListView对象让它去进行渲染展示,这就是Android使用RecyclerView / ListView进行数据展示的整体逻辑。
310 |
311 | #### 2.1 Activity+RecyclerView+Adapter小结
312 |
313 | 1. **Activity**中获取网络数据
314 |
315 | 2. **Activity**中将网络数据传递给适配器*Adapter*
316 |
317 | 3. *Adapter*中获取UI控件
318 |
319 | 4. *Adapter*中将数据绑定到对应的UI控件上
320 |
321 | 5. **Activity**中获取RecyclerView / ListView对象并设置写好的适配器
322 |
323 | Adapter中的代码细节就不过多讲了,天下所有的适配器代码逻辑大都一个样子,如果不会写就去网上找一个来模仿,也可以看我下面给出的代码,模仿一遍不行写三遍,多写几遍就会了。
324 |
325 |
326 |
327 | 👈点击展开 | 这些内容可以略过,故而折叠起来了
328 | public class RecyclerVidewAdapter extends RecyclerView.Adapter {
329 | private Context context;
330 | private List recyclerViewItemList;
331 | public RecyclerVidewAdapter() {
332 | }
333 | /**
334 | * 1、接收外部传来的数据
335 | */
336 | public RecyclerVidewAdapter(Context context, List recyclerViewItemList) {
337 | this.context = context;
338 | this.recyclerViewItemList = recyclerViewItemList;
339 | }
340 | /**
341 | * 2、填充布局
342 | */
343 | @NonNull
344 | @Override
345 | public RecyclerVidewAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
346 | View view = LayoutInflater.from(context).inflate(R.layout.item_recyclerview, parent, false);
347 | return new ViewHolder(view);
348 | }
349 | /**
350 | * 3、声明并初始化布局中的控件
351 | */
352 | public class ViewHolder extends RecyclerView.ViewHolder {
353 | private final ImageView avatar;
354 | private final TextView tv_username;
355 | private final TextView tv_createTime;
356 | private final TextView tv_content;
357 | private final NineGridViewGroup nineGridViewGroup;
358 | private final TextView tv_location;
359 | private final ImageView iv_detail_triangle;
360 | private final ImageView iv_eye;
361 | private final ImageView iv_share;
362 | public ViewHolder(@NonNull View itemView) {
363 | super(itemView);
364 | // 头像
365 | avatar = itemView.findViewById(R.id.avatar);
366 | // 用户名
367 | tv_username = itemView.findViewById(R.id.tv_username);
368 | // 创建时间
369 | tv_createTime = itemView.findViewById(R.id.tv_createTime);
370 | // 内容
371 | tv_content = itemView.findViewById(R.id.tv_content);
372 | // 图片九宫格控件
373 | nineGridViewGroup = itemView.findViewById(R.id.nineGrid);
374 | // 位置
375 | tv_location = itemView.findViewById(R.id.tv_location);
376 | // 位置详情三角小图标
377 | iv_detail_triangle = itemView.findViewById(R.id.iv_detail_triangle);
378 | // 围观眼睛小图标
379 | iv_eye = itemView.findViewById(R.id.iv_eye);
380 | // 分享小图标
381 | iv_share = itemView.findViewById(R.id.iv_share);
382 | }
383 | }
384 | /**
385 | * 4、控件上绑定数据
386 | */
387 | @Override
388 | public void onBindViewHolder(@NonNull RecyclerVidewAdapter.ViewHolder holder, int position) {
389 | // 获取对应的数据
390 | RecyclerViewItem recyclerViewItem = recyclerViewItemList.get(position);
391 | // 往控件上绑定数据
392 | setImage(context, holder.avatar, recyclerViewItem.getHeadImageUrl());
393 | holder.tv_username.setText(recyclerViewItem.getNickName());
394 | holder.tv_createTime.setText(recyclerViewItem.getCreateTime());
395 | holder.tv_content.setText(recyclerViewItem.getContent());
396 | holder.tv_location.setText(recyclerViewItem.getLocation().getAddress());
397 | holder.iv_detail_triangle.setOnClickListener(new View.OnClickListener() {
398 | @Override
399 | public void onClick(View view) {
400 | Toast.makeText(context, "位置详情图标点击事件还未开发", Toast.LENGTH_SHORT).show();
401 | }
402 | });
403 | holder.iv_eye.setOnClickListener(new View.OnClickListener() {
404 | @Override
405 | public void onClick(View view) {
406 | Toast.makeText(context, "围观图标点击事件还未开发", Toast.LENGTH_SHORT).show();
407 | }
408 | });
409 | holder.iv_share.setOnClickListener(new View.OnClickListener() {
410 | @Override
411 | public void onClick(View view) {
412 | Toast.makeText(context, "分享图标点击事件还未开发", Toast.LENGTH_SHORT).show();
413 | }
414 | });
415 | // 为满足九宫格适配器数据要求,需要构造对应的List
416 | ArrayList mediaList = recyclerViewItem.getMediaList();
417 | ArrayList nineGridItemList = new ArrayList<>();
418 | for (MyMedia myMedia : mediaList) {
419 | String thumbnailUrl = myMedia.getImageUrl();
420 | String bigImageUrl = thumbnailUrl;
421 | String videoUrl = myMedia.getVideoUrl();
422 | nineGridItemList.add(new NineGridItem(thumbnailUrl, bigImageUrl, videoUrl));
423 | }
424 | NineGridViewAdapter nineGridViewAdapter = new NineGridViewAdapter(nineGridItemList);
425 | holder.nineGridViewGroup.setAdapter(nineGridViewAdapter);
426 | }
427 | // 5、返回Item个数
428 | @Override
429 | public int getItemCount() {
430 | return recyclerViewItemList.size();
431 | }
432 | // 通过图片的Url给ImageView控件设置对应的图片
433 | private void setImage(Context context, ImageView imageView, String url) {
434 | Glide.with(context)
435 | .load(url)
436 | .placeholder(R.drawable.ic_default_color)
437 | .error(R.drawable.ic_default_color)
438 | .diskCacheStrategy(DiskCacheStrategy.ALL)
439 | .into(imageView);
440 | }
441 | }
442 |
443 |
444 |
445 | 在上面折叠的代码中,我有标记好方法的顺序,虽然一个类中,方法的顺序和功能的实现没有联系。但是我推荐你像我一样,把方法都按照这个顺序来放置,因为这样更符合逻辑:
446 |
447 | 1、适配器的构造方法,接收外部传来的数据
448 |
449 | 2、填充布局,onCreateViewHolder()
450 |
451 | 3、声明并初始化布局中的控件,ViewHolder类
452 |
453 | 4、控件上绑定数据,onBindViewHolder()
454 |
455 | 5、返回Item的个数,getItemCount()
456 |
457 |
458 |
459 | ### **---- 👆上面的代码都是在我们自己建的Project里 ---**
460 |
461 | ### **--- 👇下面的代码都是在NineGridView框架的源码里 ---**
462 |
463 |
464 |
465 |
466 | ### 3、NineGridItem.java
467 |
468 | 这个类是九宫格适配器接收数据的格式,不知道你发现规律了没有,我每次都是先从JavaBean开始说起。
469 |
470 | 这个JavaBean原先叫ImageInfo,因为既要承载 Image 又要承载 Video,将来还有可能承载 Voice概括一下就是媒体信息Media,又因为它承载的是九宫格中的一格内容,属于一个item,所以就叫成了 NineGridItem
471 |
472 | 代码也很简单
473 |
474 | ```java
475 | public class NineGridItem implements Serializable {
476 | public String thumbnailUrl;
477 | public String bigImageUrl;
478 | public int imageViewHeight;
479 | public int imageViewWidth;
480 | public int imageViewX;
481 | public int imageViewY;
482 |
483 | public String videoUrl;
484 |
485 | // 省略构造方法和Setter、Getter
486 | }
487 | ```
488 |
489 |
490 |
491 | ### 4、NineGridViewAdapter.java
492 |
493 | 这个类对外暴露
494 |
495 | 外部使用方式就是传过来一个Context,一个NineGridItem泛型的List。
496 |
497 | 再回头看这个类的主要代码,就是一个重写响应Click事件的方法,如果以概括的眼光看这个方法的话,其实只有两部分代码
498 |
499 | 如下:
500 |
501 | ```java
502 | @Override
503 | public void onNineGridItemClick(Context context, NineGridViewGroup nineGridViewGroup,
504 | int index, List NineGridItemList) {
505 | // 第一部分,计算每张图片的宽高、起始位置
506 | for (int i = 0; i < NineGridItemList.size(); i++) {
507 | NineGridItem NineGridItem = NineGridItemList.get(i);
508 | View view;
509 | if (i < nineGridViewGroup.getMaxSize()) {
510 | view = nineGridViewGroup.getChildAt(i);
511 | } else {
512 | view = nineGridViewGroup.getChildAt(nineGridViewGroup.getMaxSize() - 1);
513 | }
514 | NineGridItem.imageViewWidth = view.getWidth();
515 | NineGridItem.imageViewHeight = view.getHeight();
516 | int[] points = new int[2];
517 | view.getLocationInWindow(points);
518 | NineGridItem.imageViewX = points[0];
519 | NineGridItem.imageViewY = points[1];
520 | }
521 | // 第二部分,将带宽、高、起始位置信息的图片传递给NineGridItemDetailActivity
522 | Intent intent = new Intent(context, NineGridItemDetailActivity.class);
523 | Bundle bundle = new Bundle();
524 | bundle.putSerializable(NineGridItemDetailActivity.MEDIA_INFO, (Serializable) NineGridItemList);
525 | bundle.putInt(NineGridItemDetailActivity.CURRENT_ITEM, index);
526 | intent.putExtras(bundle);
527 | context.startActivity(intent);
528 | ((Activity) context).overridePendingTransition(0, 0);
529 | }
530 | ```
531 |
532 | #### 第一部分,计算每张图片的宽高、起始位置
533 |
534 | 我们从后端获取到的只是图片的URL地址,并没有图片相关的宽、高。图片显示多宽多高是我们根据自定义的NineGridView来计算的。
535 |
536 | 因此,我们需要遍历一下传过来的NineGridItemList中的每个NineGridItem,给他设置一个宽、高、起始坐标点。
537 |
538 | #### 第二部分,将带宽、高、起始位置信息的图片传递给NineGridItemDetailActivity
539 |
540 | 这里注意两点
541 |
542 | ① 传递自定义泛型的ListView时需要使用putSerializable()方法,而且ListView中的子类必须都实现序列化方法,即 implements Serializable,否则Intent在进行跳转的时候会报错。
543 |
544 | ② overridePendingTransition(0,0)的作用是去掉Activity切换之间的动画。
545 |
546 | 更多用法可以参考 简书 [姜康](https://www.jianshu.com/u/2c22c64b9aff) [《overridePendingTransition 的使用》](https://www.jianshu.com/p/c19e607f08aa)
547 |
548 | #### 小结:NineGridViewAdapter的作用
549 |
550 | ① 提供整个框架对外暴露的接口
551 |
552 | ② 预处理外部传过来的数据,给每个图片设置固定的九宫格宽高
553 |
554 | ③ 如果现在还不能理解自定义View,没有关系,继续往下看
555 |
556 | ### 5、NineGridItemDetailActivity.java
557 |
558 | 在上面介绍NineGridViewAdapter的第二部分代码中,有一个Intent带着NineGridItemList跳转到了NineGridItemDetailActivity。
559 |
560 | 所以这个Activity的作用就是:接收NineGridViewAdapter传递过来的图片,准备进行图片详情呈现。
561 |
562 | 这里会产生疑问:九宫格预览图片在哪呈现的,怎么感觉连图片都没有呢就点击查看详情了?
563 |
564 | 要想解开这个疑惑,详情见自定义View
565 |
566 | 话说回来图片详情呈现,我们用到了ViewPager2,这是Google公司在2019年新推出的控件,我用的这一版是1.0.0稳定版同时也是2019-11-30截止最新版。
567 |
568 | 官方说:如果可以的话,最好使用ViewPager2代替ViewPager,因为它简单,高效。
569 |
570 | 原先ViewPager的实现最经典的就是结合Fragment一起使用。现在ViewPager2可以不用实现Fragment,仅仅写一个RecyclerView的适配器,就可以了。
571 |
572 | 适配器的具体内容放到下一小节讲。这一节来看看关于ViewPager2的页面切换监听是如何实现的。
573 |
574 | 首先,我们需要在onCreate()方法中注册一个回调方法
575 |
576 | ```java
577 | viewPager2.registerOnPageChangeCallback(onPageChangeCallback);
578 | ```
579 |
580 | 为了防止onCreate()方法过于臃肿,把它的实现写到外面了
581 |
582 | ```java
583 | ViewPager2.OnPageChangeCallback onPageChangeCallback = new ViewPager2.OnPageChangeCallback() {
584 | @Override
585 | public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
586 | super.onPageScrolled(position, positionOffset, positionOffsetPixels);
587 | }
588 | /**
589 | * 页面切换结束位置: position
590 | */
591 | @SuppressLint("StringFormatMatches")
592 | @Override
593 | public void onPageSelected(int position) {
594 | super.onPageSelected(position);
595 | // 记录位置变化
596 | lastItem = currentItem;
597 | currentItem = position;
598 | // 更新底部文字
599 | tv_pager.setText(String.format(getString(R.string.select), currentItem + 1, nineGridItemList.size()));
600 | // 大于0说明有播放
601 | if (GSYVideoManager.instance().getPlayPosition() >= 0) {
602 | //当前播放的位置
603 | int currentPlayingPosition = GSYVideoManager.instance().getPlayPosition();
604 | if (currentPlayingPosition != currentItem) {
605 | GSYVideoManager.onPause();
606 | if (!GSYVideoManager.isFullState((Activity) context)) {
607 | GSYVideoManager.releaseAllVideos();
608 | viewPager2Adapter.notifyDataSetChanged();
609 | }
610 | }
611 | }
612 | }
613 | @Override
614 | public void onPageScrollStateChanged(int state) {
615 | super.onPageScrollStateChanged(state);
616 | }
617 | };
618 | ```
619 |
620 | 这里有三个方法被重写了
621 |
622 | ```java
623 | public void onPageScrolled()
624 | public void onPageSelected(int position)
625 | public void onPageScrollStateChanged()
626 | ```
627 |
628 | 我们只用到了中间这一个。这个方法带有一个参数position,它代表我们想要去的位置,也就是页面切换结束的位置。
629 |
630 | 所以,在切换之前,我们先把当前的位置记录一下。因为我们只能一页一页的切换,所以只需要把当前页面的位置+1就是下一页的序号了,然后更新底部文字。
631 |
632 | 接下来,我们要判断,如果有视频正在播放的话,看一下是不是当前位置正在播放视频。如果不是,就把正在播放的视频暂停,回收资源。
633 |
634 | 至于其他两个方法,这里没有用到(其实是我还没有掌握:)
635 |
636 | ### 6、ViewPager2Adapter.java
637 |
638 | 上面说到了谷歌官方2019年推出的新控件ViewPager2需要且仅仅需要一个RecyclerView的视频器就能实现页面切换。所以从这个适配器类的声明方式也可以看出来,其实他就是一个RecyclerView适配器
639 |
640 | ```java
641 | public class ViewPager2Adapter extends RecyclerView.Adapter
642 | ```
643 |
644 | 既然是RecyclerView的适配器,那么代码的写法就固定了,还是那五步
645 |
646 | 1、适配器的构造方法,接收外部传来的数据
647 |
648 | 2、填充布局,onCreateViewHolder()
649 |
650 | 3、声明并初始化布局中的控件,ViewHolder类
651 |
652 | 4、控件上绑定数据,onBindViewHolder()
653 |
654 | 5、返回Item的个数,getItemCount()
655 |
656 | 这里,我们重点关注一下第4步onBindViewHolder()
657 |
658 | ```java
659 | @Override
660 | public void onBindViewHolder(@NonNull ViewPager2Adapter.ViewHolder holder, int position) {
661 | // 存在视频地址
662 | if (existVideoUrl(nineGridItemList, position)) {
663 | holder.photoView.setVisibility(View.INVISIBLE);
664 | holder.gsyVideoPlayer.setVisibility(View.VISIBLE);
665 | holder.gsyVideoPlayer.setPlayTag(TAG);
666 | holder.gsyVideoPlayer.setUpLazy(
667 | nineGridItemList.get(position).getVideoUrl(),
668 | true,
669 | null,
670 | null,
671 | "");
672 | // 隐藏title
673 | holder.gsyVideoPlayer.getTitleTextView().setVisibility(View.GONE);
674 | // 隐藏返回键
675 | holder.gsyVideoPlayer.getBackButton().setVisibility(View.GONE);
676 | // 设置全屏按键功能
677 | holder.gsyVideoPlayer.getFullscreenButton().setOnClickListener(new View.OnClickListener() {
678 | @Override
679 | public void onClick(View v) {
680 | holder.gsyVideoPlayer.startWindowFullscreen(context, false, true);
681 | }
682 | });
683 | // 防止错位设置
684 | holder.gsyVideoPlayer.setPlayPosition(position);
685 | // 是否根据视频尺寸,自动选择竖屏全屏或者横屏全屏
686 | holder.gsyVideoPlayer.setAutoFullWithSize(true);
687 | // 音频焦点冲突时是否释放
688 | holder.gsyVideoPlayer.setReleaseWhenLossAudio(false);
689 | // 全屏动画
690 | holder.gsyVideoPlayer.setShowFullAnimation(true);
691 | // 小屏时不触摸滑动
692 | holder.gsyVideoPlayer.setIsTouchWiget(false);
693 | } else {
694 | holder.photoView.setVisibility(View.VISIBLE);
695 | holder.gsyVideoPlayer.setVisibility(View.INVISIBLE);
696 |
697 | showExcessPic(nineGridItemList.get(position), holder.photoView);
698 | NineGridViewGroup.getImageLoader().onDisplayImage(
699 | context,
700 | holder.photoView,
701 | nineGridItemList.get(position).bigImageUrl);
702 | }
703 | }
704 | ```
705 |
706 | 核心就是一个if条件判断语句,如果我们接收到的数据中,当前Item存在视频地址,那么就去播放视频,否则去呈现图片的大图。至于视频播放的那部分代码,是我从GSYVideoPlayer的Github地址上Copy过来的。人家专门说到了如何在列表里使用。
707 |
708 | ### 7、自定义View
709 |
710 | ### NineGridViewGroup.java
711 |
712 | 其实整个框架的核心就是这个类。自定义View这里我也不是很熟练,就说说我的理解吧。
713 |
714 | 因为我们需要呈现一个九宫格,算是一组基本的控件,所以需要继承ViewGroup
715 |
716 | ```java
717 | public class NineGridViewGroup extends ViewGroup
718 | ```
719 |
720 | 又因为ViewGroup是个抽象类,里面有个抽象方法onLayout(),根据Java规定,实现类必须实现抽象类中的抽象方法,所以我们这里必须去实现onLayout(),下面代码都是前辈写的,我们一起来欣赏一下。
721 |
722 | ### onLayout()
723 |
724 | ```java
725 | @Override
726 | protected void onLayout(boolean changed, int l, int t, int r, int b) {
727 | if (nineGridItemList == null) return;
728 | int childrenCount = nineGridItemList.size();
729 | for (int i = 0; i < childrenCount; i++) {
730 | ImageView childrenView = (ImageView) getChildAt(i);
731 | int rowNum = i / columnCount;
732 | int columnNum = i % columnCount;
733 | int left = (gridWidth + gridSpacing) * columnNum + getPaddingLeft();
734 | int top = (gridHeight + gridSpacing) * rowNum + getPaddingTop();
735 | int right = left + gridWidth;
736 | int bottom = top + gridHeight;
737 | childrenView.layout(left, top, right, bottom);
738 | if (mImageLoader != null) {
739 | mImageLoader.onDisplayImage(getContext(),
740 | childrenView,
741 | nineGridItemList.get(i).thumbnailUrl);
742 | }
743 | }
744 | }
745 | ```
746 |
747 | 通过观察代码我们可以发现,onLayout()核心代码就是通过一个for循环,生成了多个ImageView,同时计算并规定了每个ImageView的上下左右的坐标位置。
748 |
749 | 这里,为了直观,我手画了一个草图,大家可以看着理解一下代码中各个参数的意思
750 |
751 | 需要注意一点就是代码中的rowNum和columNum都是从0开始算起的。
752 |
753 |
754 |
755 | 考虑到如果只有一张图片的话,不应该显示在九宫格的左上角,而是单独显示一张图片;只有两张或四张图片的话,应该两张图片占满宽度。
756 |
757 | 所以,我们必须重写一下onMeasure()方法,下面的代码也是前辈写的。我们晚辈需要做的就是,欣赏,学习,模仿。(适配2张和4张图片相关代码是我加上去的
)
758 |
759 | ### onMeasure()
760 |
761 | ```java
762 | @Override
763 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
764 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
765 | // 获取总宽度,包含padding值
766 | int width = MeasureSpec.getSize(widthMeasureSpec);
767 | int height = 0;
768 | int totalWidth = width - getPaddingLeft() - getPaddingRight();
769 | int gridNum = nineGridItemList.size();
770 | if (nineGridItemList != null && gridNum > 0) {
771 | // 只有一张图片的情况
772 | if (gridNum == 1) {
773 | gridWidth = singleMediaSize > totalWidth ? totalWidth : singleMediaSize;
774 | gridHeight = (int) (gridWidth / singleImageRatio);
775 | //矫正图片显示区域大小,不允许超过最大显示范围
776 | if (gridHeight > singleMediaSize) {
777 | float ratio = singleMediaSize * 1.0f / gridHeight;
778 | gridWidth = (int) (gridWidth * ratio);
779 | gridHeight = singleMediaSize;
780 | }
781 | }
782 | // 有 2 张 或 4 张图片的情况
783 | else if (gridNum == 2 || gridNum == 4){
784 | gridWidth = singleMediaSize > totalWidth / 2 ? totalWidth/2 : singleMediaSize;
785 | gridHeight = (int) (gridWidth / singleImageRatio);
786 | //矫正图片显示区域大小,不允许超过最大显示范围
787 | if (gridHeight > singleMediaSize) {
788 | float ratio = singleMediaSize * 1.0f / gridHeight;
789 | gridWidth = (int) (gridWidth * ratio);
790 | gridHeight = singleMediaSize;
791 | }
792 | }
793 | // 有其他数量多张图片的情况
794 | else {
795 | // gridWidth = gridHeight = (totalWidth - gridSpacing * (columnCount - 1)) / columnCount;
796 | //这里无论是几张图片,宽高都按总宽度的 1/3
797 | gridHeight = (totalWidth - gridSpacing * 2) / 3;
798 | gridWidth = gridHeight;
799 | }
800 | width = gridWidth * columnCount + gridSpacing * (columnCount - 1) + getPaddingLeft() + getPaddingRight();
801 | height = gridHeight * rowCount + gridSpacing * (rowCount - 1) + getPaddingTop() + getPaddingBottom();
802 | }
803 | // 存储计算得到的ViewGroup的宽高
804 | setMeasuredDimension(width, height);
805 | }
806 | ```
807 |
808 | 自定义View的最后一点,我们控件要想接收数据,需要写一个适配器来作为数据的入口,这样,别人在用我们自定义View的时候,就能像使用RecyclerView / ListView一样,写个适配器,设置给对应的控件。
809 |
810 | ### setAdapter()
811 |
812 | ```java
813 | /**
814 | * 设置适配器
815 | * */
816 | public void setAdapter(@NonNull NineGridViewAdapter adapter) {
817 | // 初始化mAdapter,此类的其他方法会用到
818 | mAdapter = adapter;
819 | List nineGridItemList = adapter.getNineGridItemList();
820 |
821 | if (nineGridItemList == null || nineGridItemList.isEmpty()) {
822 | this.setVisibility(GONE);
823 | return;
824 | } else {
825 | this.setVisibility(VISIBLE);
826 | }
827 |
828 | int gridCount = nineGridItemList.size();
829 | if (maxGridSize > 0 && gridCount > maxGridSize) {
830 | nineGridItemList = nineGridItemList.subList(0, maxGridSize);
831 | gridCount = nineGridItemList.size(); //再次获取图片数量
832 | }
833 |
834 | //默认是3列显示,行数根据图片的数量决定
835 | rowCount = gridCount / 3 + (gridCount % 3 == 0 ? 0 : 1);
836 | columnCount = 3;
837 | //grid模式下,显示4张使用2X2模式
838 | if (mode == MODE_GRID) {
839 | if (gridCount == 4) {
840 | rowCount = 2;
841 | columnCount = 2;
842 | }
843 | }
844 |
845 | // 保证View的复用,避免重复创建
846 | if (this.nineGridItemList == null) {
847 | for (int i = 0; i < gridCount; i++) {
848 | ImageView iv = getImageView(i);
849 | if (iv == null)
850 | return;
851 | addView(iv, generateDefaultLayoutParams());
852 | }
853 | } else {
854 | int oldViewCount = this.nineGridItemList.size();
855 | int newViewCount = gridCount;
856 | if (oldViewCount > newViewCount) {
857 | removeViews(newViewCount, oldViewCount - newViewCount);
858 | } else if (oldViewCount < newViewCount) {
859 | for (int i = oldViewCount; i < newViewCount; i++) {
860 | ImageView iv = getImageView(i);
861 | if (iv == null)
862 | return;
863 | addView(iv, generateDefaultLayoutParams());
864 | }
865 | }
866 | }
867 | //修改最后一个条目,决定是否显示更多
868 | if (adapter.getNineGridItemList().size() > maxGridSize) {
869 | View child = getChildAt(maxGridSize - 1);
870 | if (child instanceof NineGridItemWrapperView) {
871 | NineGridItemWrapperView imageView = (NineGridItemWrapperView) child;
872 | imageView.setMoreNum(adapter.getNineGridItemList().size() - maxGridSize);
873 | }
874 | }
875 | this.nineGridItemList = nineGridItemList;
876 | // 请求重新布局
877 | requestLayout();
878 | }
879 | ```
880 |
881 | 这种自定义View的适配器写起来都是毫无章法,不像官方的RecyclerViewAdapter和BaseAdapter那样,都规定好了第一步写啥,第二步写啥。
882 |
883 | 只能去记忆、积累了,或许写的多了就会了。
884 |
885 | ## 总结
886 |
887 | ### 涉及到Android中测量尺寸的几个方法
888 |
889 | ```java
890 | // 测量图片在当前窗口的起始坐标(NineGridViewAdapter.java)
891 | int[] points = new int[2];
892 | nineGridViewItem.getLocationInWindow(points);
893 | NineGridItem.imageViewX = points[0];
894 | NineGridItem.imageViewY = points[1];
895 | // 测量整个屏幕的宽、高(NineGridItemDetailActivity.java)
896 | DisplayMetrics metric = new DisplayMetrics();
897 | getWindowManager().getDefaultDisplay().getMetrics(metric);
898 | screenWidth = metric.widthPixels;
899 | screenHeight = metric.heightPixels;
900 | // 获取自定义布局的总宽度,包含padding值(NineGridViewGroup.java)
901 | int width = MeasureSpec.getSize(widthMeasureSpec);
902 | ```
903 |
904 |
905 |
906 | ### 代码的关键路径
907 |
908 | 俗话说:“能坐着就不站着,能躺着就不坐着”。哦,不对。应该是俗话说:“一图胜千言!”
909 |
910 | 接下来,有请翠花上酸~~上图片
911 |
912 |
913 |
914 |
915 |
916 | 洋洋洒洒写了8k+字,还有一些比如属性动画还没有写到。由于我还没有完全掌握,这里就先刨个坑吧,以后熟练了再来填坑。
917 |
918 |
919 |
920 | ## 感谢 

921 |
922 | 感谢以下前辈做出的贡献(排名不分先后),晚辈只是踩在了巨人的肩膀上,换句话说,我只是抱了大佬的腿
923 |
924 | 
衷心感谢以下前辈们,致敬!
925 |
926 | ...
927 | | 序号 | 来源 | 作者 | 标题 |
928 | | ---- | ------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
929 | | 1 | Github | [廖子尧](https://github.com/jeasonlzy/NineGridView) | [《NineGridView》](https://github.com/jeasonlzy/NineGridView) |
930 | | 2 | Github | [laobie](https://github.com/laobie/NineGridImageView) | [《NineGridView》](https://github.com/laobie/NineGridImageView) |
931 | | 3 | CSDN | [sm7890123](https://blog.csdn.net/sm7890123) | [《关于Android VideoView启动时闪屏的问题》](https://blog.csdn.net/sm7890123/article/details/80801732) |
932 | | 4 | CSDN | [王英豪]() | [《Android 带你彻底理解 Window 和 WindowManager》](https://blog.csdn.net/yhaolpz/article/details/68936932) |
933 | | 5 | 简书 | [鸡汤程序员](https://www.jianshu.com/u/3f3c4485b55a) | [《Android Studio 可以正常编译但是代码爆红解决方法》](https://www.jianshu.com/p/3fab5bb2ccd2) |
934 | | 6 | 简书 | [SparkInLee](https://www.jianshu.com/u/c95e489136f6) | [《Android Animator运行原理详解》](https://www.jianshu.com/p/ee7e3d79006d) |
935 | | 7 | CSDN | [小草的胖冬瓜](https://blog.csdn.net/Yoryky) | [《最新android sdk版本号和sdk的对应关系》](https://blog.csdn.net/Yoryky/article/details/78973690) |
936 | | 8 | 简书 | [姜康](https://www.jianshu.com/u/2c22c64b9aff) | [《overridePendingTransition 的使用》](https://www.jianshu.com/p/c19e607f08aa) |
937 | | 9 | CSDN | [卖火柴的小男孩2019](https://blog.csdn.net/u014644594) | [《android 在自定义view中获取屏幕宽度,并设置自定义控件位置》](https://blog.csdn.net/u014644594/article/details/80526481) |
938 | | 10 | 博客园 | [LeoBoy](https://www.cnblogs.com/LeoBoy/) | [《BigDecimal除法运算的一个坑》](https://www.cnblogs.com/LeoBoy/p/5897754.html) |
939 | | 11 | 博客园 | [朝着希望前进](https://www.cnblogs.com/LiuDanK/) | [《解决AndroidStudio 安卓模拟器安装在D盘问题》](https://www.cnblogs.com/LiuDanK/articles/10106473.html) |
940 | | 12 | 博客园 | [弗兰克的猫](https://www.cnblogs.com/mfrank/category/1118474.html) | [《Java提高篇的若干文章》](https://www.cnblogs.com/mfrank/category/1118474.html) |
941 | | 13 | CSDN | [code丶forward](https://blog.csdn.net/xingpidong) | [《Android视频播放之ViewPager+VideoView》](https://blog.csdn.net/xingpidong/article/details/52933185) |
942 | | 14 | Github | [CarGuo](https://github.com/CarGuo) | [《GSYVideoPlayer》](https://github.com/CarGuo/GSYVideoPlayer) |
943 | | 15 | 知乎 | [相机拿反了](https://www.zhihu.com/people/xin-hui-fei-xiang) | [《十种快速压缩gif的方法,一定要收藏》](https://zhuanlan.zhihu.com/p/28866581) |
944 | | ... | ... | 还有很多大佬
没有记住名字 | 很多很多优秀的文章,我没顾上搬到这里,在此一并表示感谢! |
945 |
946 |
947 | 另外感谢一些工具、文档、资料网站
948 |
949 | 1. PC版QQ [Ctrl+Alt+A截长图工具]()
950 | 2. ProcessOn [在线画流程图]()
951 | 3. 多吉搜索 [号称要干掉百度的不追踪广告少的搜索引擎]()
952 |
953 | 还有一些Android学习站点推荐
954 |
955 | 1. [泡在网上的日子](http://www.jcodecraeer.com/)
956 | 2. [掘金](https://juejin.im/)
957 | 3. [Android官方开发者平台国内版](https://developer.android.google.cn/)
958 |
959 | 再次感谢以上前辈们,致敬!

960 | 引用一位职业旅行家小鹏说过的话作为结尾吧:
961 |
962 | “我希望每个人都有梦想,
963 |
964 | 希望每个梦想都很灿烂,
965 |
966 | 希望每个灿烂都能实现。”
--------------------------------------------------------------------------------
/image/1573992043854.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/1573992043854.png
--------------------------------------------------------------------------------
/image/1573992214487.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/1573992214487.png
--------------------------------------------------------------------------------
/image/1573992259378.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/1573992259378.png
--------------------------------------------------------------------------------
/image/1573993820069.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/1573993820069.png
--------------------------------------------------------------------------------
/image/1573996205651.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/1573996205651.png
--------------------------------------------------------------------------------
/image/1573996347984.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/1573996347984.png
--------------------------------------------------------------------------------
/image/1574087351451.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/1574087351451.png
--------------------------------------------------------------------------------
/image/1574087619014.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/1574087619014.png
--------------------------------------------------------------------------------
/image/1574088727753.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/1574088727753.png
--------------------------------------------------------------------------------
/image/1574088971183.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/1574088971183.png
--------------------------------------------------------------------------------
/image/NineGridView2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/NineGridView2.gif
--------------------------------------------------------------------------------
/image/catalog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/catalog.png
--------------------------------------------------------------------------------
/image/danshoutuosai.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/danshoutuosai.png
--------------------------------------------------------------------------------
/image/danshouyanmian.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/danshouyanmian.png
--------------------------------------------------------------------------------
/image/dianzan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/dianzan.png
--------------------------------------------------------------------------------
/image/haixiu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/haixiu.png
--------------------------------------------------------------------------------
/image/image20191130124258942.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/image20191130124258942.jpg
--------------------------------------------------------------------------------
/image/ku.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/ku.png
--------------------------------------------------------------------------------
/image/partone.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/partone.png
--------------------------------------------------------------------------------
/image/parttwo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/parttwo.png
--------------------------------------------------------------------------------
/image/penxue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/penxue.png
--------------------------------------------------------------------------------
/image/process.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/process.jpg
--------------------------------------------------------------------------------
/image/sahua.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/sahua.png
--------------------------------------------------------------------------------
/image/shuangshouwuyan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/shuangshouwuyan.png
--------------------------------------------------------------------------------
/image/tanshouwunai.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/tanshouwunai.png
--------------------------------------------------------------------------------
/image/wushanxiao.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/wushanxiao.png
--------------------------------------------------------------------------------
/image/xidatui.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/xidatui.gif
--------------------------------------------------------------------------------
/image/yiwen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/littlecurl/NineGridView/a6d156cdb0b087034df443c42ef28227117593fb/image/yiwen.png
--------------------------------------------------------------------------------