public class User extends BaseObservable implements Serializable {
public int id;
public long userId;
public String name;
public String avatar;
public String description;
public int likeCount;
public int topCommentCount;
public int followCount;
public int followerCount;
public String qqOpenId;
public long expires_time;
public int score;
public int historyCount;
public int commentCount;
public int favoriteCount;
public int feedCount;
public boolean hasFollow;
@Override
public boolean equals(@Nullable Object obj) {
if (obj == null || !(obj instanceof Feed))
return false;
User newUser = (User) obj;
return TextUtils.equals(name, newUser.name)
&& TextUtils.equals(avatar, newUser.avatar)
&& TextUtils.equals(description, newUser.description)
&& likeCount == newUser.likeCount
&& topCommentCount == newUser.topCommentCount
&& followCount == newUser.followCount
&& followerCount == newUser.followerCount
&& qqOpenId == newUser.qqOpenId
&& expires_time == newUser.expires_time
&& score == newUser.score
&& historyCount == newUser.historyCount
&& commentCount == newUser.commentCount
&& favoriteCount == newUser.favoriteCount
&& feedCount == newUser.feedCount
&& hasFollow == newUser.hasFollow;
}
@Bindable
public boolean isHasFollow() {
return hasFollow;
}
public void setHasFollow(boolean hasFollow) {
this.hasFollow = hasFollow;
notifyPropertyChanged(com.mooc.ppjoke.BR._all);
}
}
Comment
public class Comment extends BaseObservable implements Serializable {
public static final int COMMENT_TYPE_VIDEO = 3;
public static final int COMMENT_TYPE_IMAGE_TEXT = 2;
public int id;
public long itemId;
public long commentId;
public long userId;
public int commentType;
public long createTime;
public int commentCount;
public int likeCount;
public String commentText;
public String imageUrl;
public String videoUrl;
public int width;
public int height;
public boolean hasLiked;
public User author;
public Ugc ugc;
@Override
public boolean equals(@Nullable Object obj) {
if (obj == null || !(obj instanceof Comment))
return false;
Comment newComment = (Comment) obj;
return likeCount == newComment.likeCount
&& hasLiked == newComment.hasLiked
&& (author != null && author.equals(newComment.author))
&& (ugc != null && ugc.equals(newComment.ugc));
}
public Ugc getUgc() {
if (ugc == null) {
ugc = new Ugc();
}
return ugc;
}
}
Ugc(帖子的点赞评论分享)
public class Ugc extends BaseObservable implements Serializable {
public int likeCount;
@Bindable
public int getShareCount() {
return shareCount;
}
public void setShareCount(int shareCount) {
this.shareCount = shareCount;
notifyPropertyChanged(BR._all);
}
public int shareCount;
public int commentCount;
public boolean hasFavorite;
public boolean hasdiss;
@Bindable
public boolean isHasdiss() {
return hasdiss;
}
public void setHasdiss(boolean hasdiss) {
if (this.hasdiss == hasdiss)
return;
if (hasdiss) {
setHasLiked(false);
}
this.hasdiss = hasdiss;
notifyPropertyChanged(BR._all);
}
public boolean hasLiked;
@Bindable
public boolean isHasLiked() {
return hasLiked;
}
public void setHasLiked(boolean hasLiked) {
if (this.hasLiked == hasLiked)
return;
if (hasLiked) {
likeCount = likeCount + 1;
setHasdiss(false);
} else {
likeCount = likeCount - 1;
}
this.hasLiked = hasLiked;
notifyPropertyChanged(BR._all);
}
@Override
public boolean equals(@Nullable Object obj) {
if (obj == null || !(obj instanceof Ugc))
return false;
Ugc newUgc = (Ugc) obj;
return likeCount == newUgc.likeCount
&& shareCount == newUgc.shareCount
&& commentCount == newUgc.commentCount
&& hasFavorite == newUgc.hasFavorite
&& hasLiked == newUgc.hasLiked
&& hasdiss == newUgc.hasdiss;
}
@Bindable
public boolean isHasFavorite() {
return hasFavorite;
}
public void setHasFavorite(boolean hasFavorite) {
this.hasFavorite = hasFavorite;
notifyPropertyChanged(BR._all);
}
}
Feed(帖子)
public class Feed extends BaseObservable implements Serializable {
public static final int TYPE_IMAGE_TEXT = 1;//图文
public static final int TYPE_VIDEO = 2;//视频
public int id;
public long itemId;
public int itemType;
public long createTime;
public double duration;
public String feeds_text;
public long authorId;
public String activityIcon;
public String activityText;
public int width;
public int height;
public String url;
public String cover;
public User author;
public Comment topComment;
public Ugc ugc;
@Bindable
public Ugc getUgc() {
if (ugc == null) {
ugc = new Ugc();
}
return ugc;
}
@Bindable
public User getAuthor() {
return author;
}
@Override
public boolean equals(@Nullable Object obj) {
if (obj == null || !(obj instanceof Feed))
return false;
Feed newFeed = (Feed) obj;
return id == newFeed.id
&& itemId == newFeed.itemId
&& itemType == newFeed.itemType
&& createTime == newFeed.createTime
&& duration == newFeed.duration
&& TextUtils.equals(feeds_text, newFeed.feeds_text)
&& authorId == newFeed.authorId
&& TextUtils.equals(activityIcon, newFeed.activityIcon)
&& TextUtils.equals(activityText, newFeed.activityText)
&& width == newFeed.width
&& height == newFeed.height
&& TextUtils.equals(url, newFeed.url)
&& TextUtils.equals(cover, newFeed.cover)
&& (author != null && author.equals(newFeed.author))
&& (topComment != null && topComment.equals(newFeed.topComment))
&& (ugc != null && ugc.equals(newFeed.ugc));
}
}
创建帖子的作者视图
使用databinding,记得在gradle中开启
dataBinding{
enabled=true
}
在使用头像的url是我们并不能直接使用user.avatar 要创建相对应的自定义的Imageview
自定义PPImageView
dataBinding中自定义属性依赖于注解@BindingAdapter
标记方法提供给布局文件使用
- 作用于方法,这个方法可以写在任何地方
- 这个注解面向一个public static方法
- {}内部表示使用时这个属性的名字。例如:@BindingAdapter({“imageUrl”}),imageUrl为属性的名称
这里我们使用glide库来加载图片
public class PPImageView extends AppCompatImageView {
public PPImageView(Context context) {
super(context);
}
public PPImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public PPImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
ViewHelper.setViewOutline(this, attrs, defStyleAttr, 0);
}
public void setImageUrl(String imageUrl) {
setImageUrl(this, imageUrl, false);
}
@BindingAdapter(value = {"image_url", "isCircle"})
public static void setImageUrl(PPImageView view, String imageUrl, boolean isCircle) {
view.setImageUrl(view, imageUrl, isCircle, 0);
}
@BindingAdapter(value = {"image_url", "isCircle", "radius"}, requireAll = false)
public static void setImageUrl(PPImageView view, String imageUrl, boolean isCircle, int radius) {
RequestBuilder builder = Glide.with(view).load(imageUrl);
if (isCircle) {
builder.transform(new CircleCrop());
} else if (radius > 0) {
builder.transform(new RoundedCornersTransformation(PixUtils.dp2px(radius), 0));
}
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
if (layoutParams != null && layoutParams.width > 0 && layoutParams.height > 0) {
builder.override(layoutParams.width, layoutParams.height);
}
builder.into(view);
}
public void bindData(int widthPx, int heightPx, int marginLeft, String imageUrl) {
bindData(widthPx, heightPx, marginLeft, PixUtils.getScreenWidth(), PixUtils.getScreenWidth(), imageUrl);
}
public void bindData(int widthPx, int heightPx, final int marginLeft, final int maxWidth, final int maxHeight, String imageUrl) {
if (TextUtils.isEmpty(imageUrl)) {
setVisibility(GONE);
return;
} else {
setVisibility(VISIBLE);
}
if (widthPx <= 0 || heightPx <= 0) {
Glide.with(this).load(imageUrl).into(new SimpleTarget() {
@Override
public void onResourceReady(@NonNull Drawable resource, @Nullable Transition super Drawable> transition) {
int height = resource.getIntrinsicHeight();
int width = resource.getIntrinsicWidth();
setSize(width, height, marginLeft, maxWidth, maxHeight);
setImageDrawable(resource);
}
});
return;
}
setSize(widthPx, heightPx, marginLeft, maxWidth, maxHeight);
setImageUrl(this, imageUrl, false);
}
private void setSize(int width, int height, int marginLeft, int maxWidth, int maxHeight) {
int finalWidth, finalHeight;
if (width > height) {
finalWidth = maxWidth;
finalHeight = (int) (height / (width * 1.0f / finalWidth));
} else {
finalHeight = maxHeight;
finalWidth = (int) (width / (height * 1.0f / finalHeight));
}
ViewGroup.LayoutParams params = getLayoutParams();
params.width = finalWidth;
params.height = finalHeight;
if (params instanceof FrameLayout.LayoutParams) {
((FrameLayout.LayoutParams) params).leftMargin = height > width ? PixUtils.dp2px(marginLeft) : 0;
} else if (params instanceof LinearLayout.LayoutParams) {
((LinearLayout.LayoutParams) params).leftMargin = height > width ? PixUtils.dp2px(marginLeft) : 0;
}
setLayoutParams(params);
}
@BindingAdapter(value = {"blur_url", "radius"})
public static void setBlurImageUrl(ImageView imageView, String blurUrl, int radius) {
Glide.with(imageView).load(blurUrl).override(radius)
.transform(new BlurTransformation())
.dontAnimate()
.into(new SimpleTarget() {
@Override
public void onResourceReady(@NonNull Drawable resource, @Nullable Transition super Drawable> transition) {
imageView.setBackground(resource);
}
});
}
}
创建帖子文字内容的视图
要导入工具类TextUtils判断文字是否为空,若为空则设置textview的visibility为gone
创建帖子图片类型tag的视图
同样也需要导入工具类
MaterialButton有内置属性 如果需要更改则需要自定义
在style下自定义
自定义完后应用到AppTheme中
神评区域- @style/materialButton
- 点赞数超过一万不能直接显示 要进行一个转化,创建StringConvert
public class StringConvert {
public static String convertFeedUgc(int count) {
if (count < 10000) {
return String.valueOf(count);
}
return count / 10000 + "万";
}
public static String convertTagFeedList(int num) {
if (num < 10000) {
return num + "人观看";
} else {
return num / 10000 + "万人观看";
}
}
public static CharSequence convertSpannable(int count, String intro) {
String countStr = String.valueOf(count);
SpannableString ss = new SpannableString(countStr + intro);
ss.setSpan(new ForegroundColorSpan(Color.BLACK), 0, countStr.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ss.setSpan(new AbsoluteSizeSpan(16, true), 0, countStr.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
ss.setSpan(new StyleSpan(Typeface.BOLD), 0, countStr.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return ss;
}
}
帖子互动区域
图片类型的资源文件
databinding的使用建议及构建视频列表布局
databinding在绑定数据的时候不是马上立刻执行的而是延一帧,所以如果在列表中有的组件的宽和高是不确定的,需要我们在运行的时候计算出来,这时候的数据绑定就不建议使用databinding,而是使用原生的方式来绑定。
数据绑定方法写在PPImageVIew(上文有相关代码)中为binData方法
package com.mooc.ppjoke.view;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.databinding.BindingAdapter;
import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestBuilder;
import com.bumptech.glide.load.resource.bitmap.CircleCrop;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.request.target.SimpleTarget;
import com.bumptech.glide.request.transition.Transition;
import com.mooc.libcommon.utils.PixUtils;
import com.mooc.libcommon.view.ViewHelper;
import jp.wasabeef.glide.transformations.BlurTransformation;
import jp.wasabeef.glide.transformations.RoundedCornersTransformation;
public class PPImageView extends AppCompatImageView {
public PPImageView(Context context) {
super(context);
}
public PPImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public PPImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
ViewHelper.setViewOutline(this, attrs, defStyleAttr, 0);
}
public void setImageUrl(String imageUrl) {
setImageUrl(this, imageUrl, false);
}
@BindingAdapter(value = {"image_url", "isCircle"})
public static void setImageUrl(PPImageView view, String imageUrl, boolean isCircle) {
view.setImageUrl(view, imageUrl, isCircle, 0);
}
@BindingAdapter(value = {"image_url", "isCircle", "radius"}, requireAll = false)
public static void setImageUrl(PPImageView view, String imageUrl, boolean isCircle, int radius) {
RequestBuilder builder = Glide.with(view).load(imageUrl);
if (isCircle) {
builder.transform(new CircleCrop());
} else if (radius > 0) {
builder.transform(new RoundedCornersTransformation(PixUtils.dp2px(radius), 0));
}
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
if (layoutParams != null && layoutParams.width > 0 && layoutParams.height > 0) {
builder.override(layoutParams.width, layoutParams.height);
}
builder.into(view);
}
public void bindData(int widthPx, int heightPx, int marginLeft, String imageUrl) {
bindData(widthPx, heightPx, marginLeft, PixUtils.getScreenWidth(), PixUtils.getScreenWidth(), imageUrl);
}
public void bindData(int widthPx, int heightPx, final int marginLeft, final int maxWidth, final int maxHeight, String imageUrl) {
if (TextUtils.isEmpty(imageUrl)) {
setVisibility(GONE);
return;
} else {
setVisibility(VISIBLE);
}
if (widthPx <= 0 || heightPx <= 0) {
Glide.with(this).load(imageUrl).into(new SimpleTarget() {
@Override
public void onResourceReady(@NonNull Drawable resource, @Nullable Transition super Drawable> transition) {
int height = resource.getIntrinsicHeight();
int width = resource.getIntrinsicWidth();
setSize(width, height, marginLeft, maxWidth, maxHeight);
setImageDrawable(resource);
}
});
return;
}
setSize(widthPx, heightPx, marginLeft, maxWidth, maxHeight);
setImageUrl(this, imageUrl, false);
}
private void setSize(int width, int height, int marginLeft, int maxWidth, int maxHeight) {
int finalWidth, finalHeight;
if (width > height) {
finalWidth = maxWidth;
finalHeight = (int) (height / (width * 1.0f / finalWidth));
} else {
finalHeight = maxHeight;
finalWidth = (int) (width / (height * 1.0f / finalHeight));
}
ViewGroup.LayoutParams params = getLayoutParams();
params.width = finalWidth;
params.height = finalHeight;
if (params instanceof FrameLayout.LayoutParams) {
((FrameLayout.LayoutParams) params).leftMargin = height > width ? PixUtils.dp2px(marginLeft) : 0;
} else if (params instanceof LinearLayout.LayoutParams) {
((LinearLayout.LayoutParams) params).leftMargin = height > width ? PixUtils.dp2px(marginLeft) : 0;
}
setLayoutParams(params);
}
@BindingAdapter(value = {"blur_url", "radius"})
public static void setBlurImageUrl(ImageView imageView, String blurUrl, int radius) {
Glide.with(imageView).load(blurUrl).override(radius)
.transform(new BlurTransformation())
.dontAnimate()
.into(new SimpleTarget() {
@Override
public void onResourceReady(@NonNull Drawable resource, @Nullable Transition super Drawable> transition) {
imageView.setBackground(resource);
}
});
}
}
PixUtils(common包下)
public class PixUtils {
public static int dp2px(int dpValue) {
DisplayMetrics metrics = AppGlobals.getApplication().getResources().getDisplayMetrics();
return (int) (metrics.density * dpValue + 0.5f);
}
public static int getScreenWidth() {
DisplayMetrics metrics = AppGlobals.getApplication().getResources().getDisplayMetrics();
return metrics.widthPixels;
}
public static int getScreenHeight() {
DisplayMetrics metrics = AppGlobals.getApplication().getResources().getDisplayMetrics();
return metrics.heightPixels;
}
}
及构建视频列表布局(自定义view)
其他部分和图片布局的资源文件只是将图片布局的PPImageView替换成我们自定义的视频区域
自定义ListPlayerView继承自FrameLayout
public class ListPlayerView extends FrameLayout implements IPlayTarget, PlayerControlView.VisibilityListener, Player.EventListener {
public View bufferView;
public PPImageView cover, blur;
protected ImageView playBtn;
protected String mCategory;
protected String mVideoUrl;
protected boolean isPlaying;
protected int mWidthPx;
protected int mHeightPx;
public ListPlayerView(@NonNull Context context) {
this(context, null);
}
public ListPlayerView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public ListPlayerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public ListPlayerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
LayoutInflater.from(context).inflate(R.layout.layout_player_view, this, true);
//缓冲转圈圈的view
bufferView = findViewById(R.id.buffer_view);
//封面view
cover = findViewById(R.id.cover);
//高斯模糊背景图,防止出现两边留嘿
blur = findViewById(R.id.blur_background);
//播放盒暂停的按钮
playBtn = findViewById(R.id.play_btn);
playBtn.setOnClickListener(v -> {
if (isPlaying()) {
inActive();
} else {
onActive();
}
});
this.setTransitionName("listPlayerView");
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//点击该区域时 我们诸主动让视频控制器显示出来
PageListPlay pageListPlay = PageListPlayManager.get(mCategory);
pageListPlay.controlView.show();
return true;
}
public void bindData(String category, int widthPx, int heightPx, String coverUrl, String videoUrl) {
mCategory = category;
mVideoUrl = videoUrl;
mWidthPx = widthPx;
mHeightPx = heightPx;
cover.setImageUrl(coverUrl);
//如果该视频的宽度小于高度,则高斯模糊背景图显示出来
if (widthPx < heightPx) {
PPImageView.setBlurImageUrl(blur, coverUrl, 10);
blur.setVisibility(VISIBLE);
} else {
blur.setVisibility(INVISIBLE);
}
setSize(widthPx, heightPx);
}
protected void setSize(int widthPx, int heightPx) {
//这里主要是做视频宽大与高,或者高大于宽时 视频的等比缩放
int maxWidth = PixUtils.getScreenWidth();
int maxHeight = maxWidth;
int layoutWidth = maxWidth;
int layoutHeight = 0;
int coverWidth;
int coverHeight;
if (widthPx >= heightPx) {
coverWidth = maxWidth;
layoutHeight = coverHeight = (int) (heightPx / (widthPx * 1.0f / maxWidth));
} else {
layoutHeight = coverHeight = maxHeight;
coverWidth = (int) (widthPx / (heightPx * 1.0f / maxHeight));
}
ViewGroup.LayoutParams params = getLayoutParams();
params.width = layoutWidth;
params.height = layoutHeight;
setLayoutParams(params);
ViewGroup.LayoutParams blurParams = blur.getLayoutParams();
blurParams.width = layoutWidth;
blurParams.height = layoutHeight;
blur.setLayoutParams(blurParams);
FrameLayout.LayoutParams coverParams = (LayoutParams) cover.getLayoutParams();
coverParams.width = coverWidth;
coverParams.height = coverHeight;
coverParams.gravity = Gravity.CENTER;
cover.setLayoutParams(coverParams);
FrameLayout.LayoutParams playBtnParams = (LayoutParams) playBtn.getLayoutParams();
playBtnParams.gravity = Gravity.CENTER;
playBtn.setLayoutParams(playBtnParams);
}
@Override
public ViewGroup getOwner() {
return this;
}
@Override
public void onActive() {
//视频播放,或恢复播放
//通过该View所在页面的mCategory(比如首页列表tab_all,沙发tab的tab_video,标签帖子聚合的tag_feed) 字段,
//取出管理该页面的Exoplayer播放器,ExoplayerView播放View,控制器对象PageListPlay
PageListPlay pageListPlay = PageListPlayManager.get(mCategory);
PlayerView playerView = pageListPlay.playerView;
PlayerControlView controlView = pageListPlay.controlView;
SimpleExoPlayer exoPlayer = pageListPlay.exoPlayer;
if (playerView == null) {
return;
}
//此处我们需要主动调用一次 switchPlayerView,把播放器Exoplayer和展示视频画面的View ExoplayerView相关联
//为什么呢?因为在列表页点击视频Item跳转到视频详情页的时候,详情页会复用列表页的播放器Exoplayer,然后和新创建的展示视频画面的View ExoplayerView相关联,达到视频无缝续播的效果
//如果 我们再次返回列表页,则需要再次把播放器和ExoplayerView相关联
pageListPlay.switchPlayerView(playerView, true);
ViewParent parent = playerView.getParent();
if (parent != this) {
//把展示视频画面的View添加到ItemView的容器上
if (parent != null) {
((ViewGroup) parent).removeView(playerView);
//还应该暂停掉列表上正在播放的那个
((ListPlayerView) parent).inActive();
}
ViewGroup.LayoutParams coverParams = cover.getLayoutParams();
this.addView(playerView, 1, coverParams);
}
ViewParent ctrlParent = controlView.getParent();
if (ctrlParent != this) {
//把视频控制器 添加到ItemView的容器上
if (ctrlParent != null) {
((ViewGroup) ctrlParent).removeView(controlView);
}
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.BOTTOM;
this.addView(controlView, params);
}
//如果是同一个视频资源,则不需要从重新创建mediaSource。
//但需要onPlayerStateChanged 否则不会触发onPlayerStateChanged()
if (TextUtils.equals(pageListPlay.playUrl, mVideoUrl)) {
onPlayerStateChanged(true, Player.STATE_READY);
} else {
MediaSource mediaSource = PageListPlayManager.createMediaSource(mVideoUrl);
exoPlayer.prepare(mediaSource);
exoPlayer.setRepeatMode(Player.REPEAT_MODE_ONE);
pageListPlay.playUrl = mVideoUrl;
}
controlView.show();
controlView.setVisibilityListener(this);
exoPlayer.addListener(this);
exoPlayer.setPlayWhenReady(true);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
isPlaying = false;
bufferView.setVisibility(GONE);
cover.setVisibility(VISIBLE);
playBtn.setVisibility(VISIBLE);
playBtn.setImageResource(R.drawable.icon_video_play);
}
@Override
public void inActive() {
//暂停视频的播放并让封面图和 开始播放按钮 显示出来
PageListPlay pageListPlay = PageListPlayManager.get(mCategory);
if (pageListPlay.exoPlayer == null || pageListPlay.controlView == null || pageListPlay.exoPlayer == null)
return;
pageListPlay.exoPlayer.setPlayWhenReady(false);
pageListPlay.controlView.setVisibilityListener(null);
pageListPlay.exoPlayer.removeListener(this);
cover.setVisibility(VISIBLE);
playBtn.setVisibility(VISIBLE);
playBtn.setImageResource(R.drawable.icon_video_play);
}
@Override
public boolean isPlaying() {
return isPlaying;
}
@Override
public void onVisibilityChange(int visibility) {
playBtn.setVisibility(visibility);
playBtn.setImageResource(isPlaying() ? R.drawable.icon_video_pause : R.drawable.icon_video_play);
}
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
//监听视频播放的状态
PageListPlay pageListPlay = PageListPlayManager.get(mCategory);
SimpleExoPlayer exoPlayer = pageListPlay.exoPlayer;
if (playbackState == Player.STATE_READY && exoPlayer.getBufferedPosition() != 0 && playWhenReady) {
cover.setVisibility(GONE);
bufferView.setVisibility(GONE);
} else if (playbackState == Player.STATE_BUFFERING) {
bufferView.setVisibility(VISIBLE);
}
isPlaying = playbackState == Player.STATE_READY && exoPlayer.getBufferedPosition() != 0 && playWhenReady;
playBtn.setImageResource(isPlaying ? R.drawable.icon_video_pause : R.drawable.icon_video_play);
}
public View getPlayController() {
PageListPlay listPlay = PageListPlayManager.get(mCategory);
return listPlay.controlView;
}
}
layout_player_view
- merge必须放在布局文件的根节点上。
- merge并不是一个ViewGroup,也不是一个View,它相当于声明了一些视图,等待被添加。
- merge标签被添加到A容器下,那么merge下的所有视图将被添加到A容器下。
- 因为merge标签并不是View,所以在通过LayoutInflate.inflate方法渲染的时候,
第二个参数必须指定一个父容器,且第三个参数必须为true,也就是必须为merge下的视图指定一个父亲节点。 - 因为merge不是View,所以对merge标签设置的所有属性都是无效的。
//刷新分页组件 api 'com.scwang.smartrefresh:SmartRefreshLayout:1.1.0' api 'com.scwang.smartrefresh:SmartRefreshHeader:1.1.0' //exo播放器 api 'com.google.android.exoplayer:exoplayer-core:2.10.4' api 'com.google.android.exoplayer:exoplayer-dash:2.10.4' api 'com.google.android.exoplayer:exoplayer-ui:2.10.4' //viewmodel and livedata api 'androidx.lifecycle:lifecycle-extensions:2.1.0' //或者指明仅仅使用 livedata、viewmodel api "androidx.lifecycle:lifecycle-viewmodel:2.1.0" api 'androidx.lifecycle:lifecycle-livedata:2.1.0' //paging分页组件 api 'androidx.paging:paging-runtime:2.1.0'1.PagedList只支持查询(get),不支持增删改,如何实现呢? 2.paging如何做到上拉分页时手动触发数据请求并更新页面? 3.DiffUtil原理之[Myers 差分算法]https://www.jianshu.com/p/aac191f0b1ee 刷新的界面布局
没有数据的空布局
在初始化方法中加载一个布局
public class EmptyView extends LinearLayout {
private ImageView icon;
private TextView title;
private Button action;
public EmptyView(@NonNull Context context) {
this(context, null);
}
public EmptyView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public EmptyView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public EmptyView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int style) {
super(context, attrs, defStyleAttr, style);
setOrientation(VERTICAL);
setGravity(Gravity.CENTER);
LayoutInflater.from(context).inflate(R.layout.layout_empty_view, this, true);
icon = findViewById(R.id.empty_icon);
title = findViewById(R.id.empty_text);
action = findViewById(R.id.empty_action);
}
public void setEmptyIcon(@DrawableRes int iconRes) {
icon.setImageResource(iconRes);
}
public void setTitle(String text) {
if (TextUtils.isEmpty(text)) {
title.setVisibility(GONE);
} else {
title.setText(text);
title.setVisibility(VISIBLE);
}
}
public void setButton(String text, View.OnClickListener listener) {
if (TextUtils.isEmpty(text)) {
action.setVisibility(GONE);
} else {
action.setText(text);
action.setVisibility(VISIBLE);
action.setOnClickListener(listener);
}
}
}
初始化加载的布局
基础AbsListFragment
配置一些列表通用配置
recycler的adapter是PageListAdapter
public abstract class AbsListFragment编写HomeFragment继承AbListFragment> extends Fragment implements OnRefreshListener, OnLoadMoreListener { protected LayoutRefreshViewBinding binding; protected RecyclerView mRecyclerView; protected SmartRefreshLayout mRefreshLayout; protected EmptyView mEmptyView; protected PagedListAdapter adapter; protected M mViewModel; protected DividerItemDecoration decoration; @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { binding = LayoutRefreshViewBinding.inflate(inflater, container, false); binding.getRoot().setFitsSystemWindows(true); mRecyclerView = binding.recyclerView; mRefreshLayout = binding.refreshLayout; mEmptyView = binding.emptyView; mRefreshLayout.setEnableRefresh(true); mRefreshLayout.setEnableLoadMore(true); mRefreshLayout.setOnRefreshListener(this); mRefreshLayout.setOnLoadMoreListener(this); adapter = getAdapter(); mRecyclerView.setAdapter(adapter); mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false)); mRecyclerView.setItemAnimator(null); //默认给列表中的Item 一个 10dp的ItemDecoration decoration = new DividerItemDecoration(getContext(), LinearLayoutManager.VERTICAL); decoration.setDrawable(ContextCompat.getDrawable(getContext(), R.drawable.list_divider)); mRecyclerView.addItemDecoration(decoration); genericViewModel(); return binding.getRoot(); } private void genericViewModel() { //利用 子类传递的 泛型参数实例化出absViewModel 对象。 ParameterizedType type = (ParameterizedType) getClass().getGenericSuperclass(); Type[] arguments = type.getActualTypeArguments(); if (arguments.length > 1) { Type argument = arguments[1]; Class modelClaz = ((Class) argument).asSubclass(AbsViewModel.class); mViewModel = (M) ViewModelProviders.of(this).get(modelClaz); //触发页面初始化数据加载的逻辑 mViewModel.getPageData().observe(this, pagedList -> submitList(pagedList)); //监听分页时有无更多数据,以决定是否关闭上拉加载的动画 mViewModel.getBoundaryPageData().observe(this, hasData -> finishRefresh(hasData)); } } public void submitList(PagedList result) { //只有当新数据集合大于0 的时候,才调用adapter.submitList //否则可能会出现 页面----有数据----->被清空-----空布局 if (result.size() > 0) { adapter.submitList(result); } finishRefresh(result.size() > 0); } public void finishRefresh(boolean hasData) { PagedList currentList = adapter.getCurrentList(); hasData = hasData || currentList != null && currentList.size() > 0; RefreshState state = mRefreshLayout.getState(); if (state.isFooter && state.isOpening) { mRefreshLayout.finishLoadMore(); } else if (state.isHeader && state.isOpening) { mRefreshLayout.finishRefresh(); } if (hasData) { mEmptyView.setVisibility(View.GONE); } else { mEmptyView.setVisibility(View.VISIBLE); } } public abstract PagedListAdapter getAdapter(); }
实现上面所写的虚方法PagedListAdapter以及实现onLoadMore、onRefresh
@FragmentDestination(pageUrl = "main/tabs/home", asStarter = true) public class HomeFragment extends AbsListFragmentFeedAdapter{ private PageListPlayDetector playDetector; private String feedType; private boolean shouldPause = true; public static HomeFragment newInstance(String feedType) { Bundle args = new Bundle(); args.putString("feedType", feedType); HomeFragment fragment = new HomeFragment(); fragment.setArguments(args); return fragment; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { mViewModel.getCacheLiveData().observe(this, new Observer >() { @Override public void onChanged(PagedList feeds) { submitList(feeds); } }); playDetector = new PageListPlayDetector(this, mRecyclerView); mViewModel.setFeedType(feedType); } @Override public PagedListAdapter getAdapter() { feedType = getArguments() == null ? "all" : getArguments().getString("feedType"); return new FeedAdapter(getContext(), feedType) { @Override public void onViewAttachedToWindow2(@NonNull ViewHolder holder) { if (holder.isVideoItem()) { playDetector.addTarget(holder.getListPlayerView()); } } @Override public void onViewDetachedFromWindow2(@NonNull ViewHolder holder) { playDetector.removeTarget(holder.getListPlayerView()); } @Override public void onStartFeedDetailActivity(Feed feed) { boolean isVideo = feed.itemType == Feed.TYPE_VIDEO; shouldPause = !isVideo; } @Override public void onCurrentListChanged(@Nullable PagedList previousList, @Nullable PagedList currentList) { //这个方法是在我们每提交一次 pagelist对象到adapter 就会触发一次 //每调用一次 adpater.submitlist if (previousList != null && currentList != null) { if (!currentList.containsAll(previousList)) { mRecyclerView.scrollToPosition(0); } } } }; } @Override public void onLoadMore(@NonNull RefreshLayout refreshLayout) { final PagedList currentList = adapter.getCurrentList(); if (currentList == null || currentList.size() <= 0) { finishRefresh(false); return; } Feed feed = currentList.get(adapter.getItemCount() - 1); mViewModel.loadAfter(feed.id, new ItemKeyedDataSource.LoadCallback () { @Override public void onResult(@NonNull List data) { PagedList.Config config = currentList.getConfig(); if (data != null && data.size() > 0) { //这里 咱们手动接管 分页数据加载的时候 使用MutableItemKeyedDataSource也是可以的。 //由于当且仅当 paging不再帮我们分页的时候,我们才会接管。所以 就不需要ViewModel中创建的DataSource继续工作了,所以使用 //MutablePageKeyedDataSource也是可以的 MutablePageKeyedDataSource dataSource = new MutablePageKeyedDataSource(); //这里要把列表上已经显示的先添加到dataSource.data中 //而后把本次分页回来的数据再添加到dataSource.data中 dataSource.data.addAll(currentList); dataSource.data.addAll(data); PagedList pagedList = dataSource.buildNewPagedList(config); submitList(pagedList); } } }); } @Override public void onRefresh(@NonNull RefreshLayout refreshLayout) { //invalidate 之后Paging会重新创建一个DataSource 重新调用它的loadInitial方法加载初始化数据 //详情见:LivePagedListBuilder#compute方法 mViewModel.getDataSource().invalidate(); } @Override public void onHiddenChanged(boolean hidden) { super.onHiddenChanged(hidden); if (hidden) { playDetector.onPause(); } else { playDetector.onResume(); } } @Override public void onPause() { //如果是跳转到详情页,咱们就不需要 暂停视频播放了 //如果是前后台切换 或者去别的页面了 都是需要暂停视频播放的 if (shouldPause) { playDetector.onPause(); } Log.e("homefragment", "onPause: feedtype:" + feedType); super.onPause(); } @Override public void onResume() { super.onResume(); shouldPause = true; //由于沙发Tab的几个子页面 复用了HomeFragment。 //我们需要判断下 当前页面 它是否有ParentFragment. //当且仅当 它和它的ParentFragment均可见的时候,才能恢复视频播放 if (getParentFragment() != null) { if (getParentFragment().isVisible() && isVisible()) { Log.e("homefragment", "onResume: feedtype:" + feedType); playDetector.onResume(); } } else { if (isVisible()) { Log.e("homefragment", "onResume: feedtype:" + feedType); playDetector.onResume(); } } } @Override public void onDestroy() { //记得销毁 PageListPlayManager.release(feedType); super.onDestroy(); } }
public class FeedAdapter extends AbsPagedListAdapter{ private final LayoutInflater inflater; protected Context mContext; protected String mCategory; public FeedAdapter(Context context, String category) { super(new DiffUtil.ItemCallback () { @Override public boolean areItemsTheSame(@NonNull Feed oldItem, @NonNull Feed newItem) { return oldItem.id == newItem.id; } @Override public boolean areContentsTheSame(@NonNull Feed oldItem, @NonNull Feed newItem) { return oldItem.equals(newItem); } }); inflater = LayoutInflater.from(context); mContext = context; mCategory = category; } @Override public int getItemViewType2(int position) { Feed feed = getItem(position); if (feed.itemType == Feed.TYPE_IMAGE_TEXT) { return R.layout.layout_feed_type_image; } else if (feed.itemType == Feed.TYPE_VIDEO) { return R.layout.layout_feed_type_video; } return 0; } @Override protected ViewHolder onCreateViewHolder2(ViewGroup parent, int viewType) { ViewDataBinding binding = DataBindingUtil.inflate(inflater, viewType, parent, false); return new ViewHolder(binding.getRoot(), binding); } @Override protected void onBindViewHolder2(ViewHolder holder, int position) { final Feed feed = getItem(position); holder.bindData(feed); holder.itemView.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { FeedDetailActivity.startFeedDetailActivity(mContext, feed, mCategory); onStartFeedDetailActivity(feed); if (mFeedObserver == null) { mFeedObserver = new FeedObserver(); LiveDataBus.get() .with(InteractionPresenter.DATA_FROM_INTERACTION) .observe((LifecycleOwner) mContext, mFeedObserver); } mFeedObserver.setFeed(feed); } }); } public void onStartFeedDetailActivity(Feed feed) { } private FeedObserver mFeedObserver; private class FeedObserver implements Observer { private Feed mFeed; @Override public void onChanged(Feed newOne) { if (mFeed.id != newOne.id) return; mFeed.author = newOne.author; mFeed.ugc = newOne.ugc; mFeed.notifyChange(); } public void setFeed(Feed feed) { mFeed = feed; } } public class ViewHolder extends RecyclerView.ViewHolder { public ViewDataBinding mBinding; public ListPlayerView listPlayerView; public ImageView feedImage; public ViewHolder(@NonNull View itemView, ViewDataBinding binding) { super(itemView); mBinding = binding; } public void bindData(Feed item) { //这里之所以手动绑定数据的原因是 图片 和视频区域都是需要计算的 //而dataBinding的执行默认是延迟一帧的。 //当列表上下滑动的时候 ,会明显的看到宽高尺寸不对称的问题 mBinding.setVariable(com.mooc.ppjoke.BR.feed, item); mBinding.setVariable(BR.lifeCycleOwner, mContext); if (mBinding instanceof LayoutFeedTypeImageBinding) { LayoutFeedTypeImageBinding imageBinding = (LayoutFeedTypeImageBinding) mBinding; feedImage = imageBinding.feedImage; imageBinding.feedImage.bindData(item.width, item.height, 16, item.cover); //imageBinding.setFeed(item); //imageBinding.interactionBinding.setLifeCycleOwner((LifecycleOwner) mContext); } else if (mBinding instanceof LayoutFeedTypeVideoBinding) { LayoutFeedTypeVideoBinding videoBinding = (LayoutFeedTypeVideoBinding) mBinding; videoBinding.listPlayerView.bindData(mCategory, item.width, item.height, item.cover, item.url); listPlayerView = videoBinding.listPlayerView; //videoBinding.setFeed(item); //videoBinding.interactionBinding.setLifeCycleOwner((LifecycleOwner) mContext); } } public boolean isVideoItem() { return mBinding instanceof LayoutFeedTypeVideoBinding; } public ListPlayerView getListPlayerView() { return listPlayerView; } } }



