Android仿抖音主页效果实现代码
目录
- 一、准备工作
- 1.1、主页面布局
- 1.2、列表Item布局
- 1.3、列表Item适配器
- 二、自定义LayoutManager
- 三、实现播放
写在前面
各位老铁,我又来啦!既然来了,那肯定又来搞事情啦,话不多说,先上图!
“抖音”都玩过吧,是不是很好玩,我反正是天天刷,作为一个非著名的Android低级攻城狮,虽然技术菜的一匹,但是也经常刷着刷着会思考:咦?这玩意是用哪个控件做的?这个效果是咋实现的啊?由于本人技术水平有限,所以今天咱就先挑个比较简单的来看看是如何实现的,思考再三,我们就拿抖音首页的这个效果来练练手吧,话不多说,开搞!
一、准备工作
我们先不急着写代码,先对抖音的这种效果做一个简单的分析,首先需要明确的是它是可以滑动的,并且可以上滑回去,也可以下滑到下一个,滑动的数量跟随视频的个数而定,到这里其实能实现这种效果的控件就已经被缩小到一个范围内了。初步判定可以使用ViewPager或者是RecyclerView来实现,你细想一下,它实际上就是一个列表啊,每一屏的视频效果就是一个单独的Item,并且它的列表Item的数量可以很大,至少目前你应该没有哪一次是能把抖音滑到底的吧,那最后咱们使用RecyclerView来实现这个效果。
为什么不用ViewPager?我们需要的是每次只加载一屏,ViewPager默认会有预加载机制,并且数据量较大的时候性能表现也是很差的。反之,RecyclerView最好的性能就是只加载一屏幕的Item,并且处理海量数据时性能更优,所以我们选用RecyclerView实现。
基础列表的承载控件我们已经选好了,然后通过上面的效果不难发现,每一屏里面实际上只有一个Item,所以基础的页面布局你应该也知道该怎么做了。然后就是视频播放了,由于这里我们只是仿照实现抖音的主页面效果,最核心的实际上是实现这个RecyclerView滑动的效果,所以代码我这里是尽量考虑简单化,因此视频播放就直接使用的Android原生的VideoView来做的,效果肯定不会多好,如果你对视频播放这块要求比较高的话,可以考虑使用基于ijkplayer实现的一些比较优秀的开源库,再或者能力强的自己基于ffmpeg定制开发播放器。
OK,到这里基本的分析就已经做完了,下面我们就先来实现基础代码吧!
先把需要的图片和视频文件准备好哦,别忘记了,视频这里放在res/raw目录下:
1.1、主页面布局
新建一个TiktokIndexActivity.java,创建布局文件activity_tiktok_layout.xml:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <android.support.v7.widget.RecyclerView android:id="@+id/mRecycler" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/color_01"/> <RelativeLayout android:layout_width="match_parent" android:layout_height="35dp" android:layout_marginTop="36dp"> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:orientation="horizontal" android:gravity="center_vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="16dp" android:text="南京" android:textColor="#f2f2f2" android:textSize="18sp" android:textStyle="bold" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="16dp" android:text="关注" android:textColor="#f2f2f2" android:textSize="18sp" android:textStyle="bold" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="推荐" android:textColor="@android:color/white" android:textSize="20sp" android:textStyle="bold" /> </LinearLayout> <ImageView android:layout_width="30dp" android:layout_height="30dp" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:layout_marginRight="16dp" android:src="@drawable/search_icon" android:tint="#f2f2f2" /> </RelativeLayout> <LinearLayout android:id="@+id/mBottomLayout" android:layout_width="match_parent" android:layout_height="?actionBarSize" android:background="@color/color_01" android:layout_alignParentBottom="true" android:gravity="center_vertical" android:orientation="horizontal"> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="首页" android:textColor="@android:color/white" android:textSize="18sp" android:textStyle="bold" /> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="朋友" android:textColor="#f2f2f2" android:textSize="17sp" android:textStyle="bold" /> <LinearLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" android:gravity="center"> <ImageView android:layout_width="50dp" android:layout_height="30dp" android:scaleType="fitCenter" android:src="@drawable/icon_add" /> </LinearLayout> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="消息" android:textColor="#f2f2f2" android:textSize="17sp" android:textStyle="bold" /> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center" android:text="我" android:textColor="#f2f2f2" android:textSize="17sp" android:textStyle="bold" /> </LinearLayout> </RelativeLayout>
1.2、列表Item布局
由于我们的VideoView想要自己设置宽和高,所以这里自定义一个VideoView,重写onMeasure()测量方法:
public class CusVideoView extends VideoView { public CusVideoView(Context context) { super(context); } public CusVideoView(Context context, AttributeSet attrs) { super(context, attrs); } public CusVideoView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int width = getDefaultSize(getWidth(), widthMeasureSpec); int height = getDefaultSize(getHeight(), heightMeasureSpec); setMeasuredDimension(width, height); } }
然后接着来编写每一屏Item的布局文件:item_tiktok_layout.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/mRootView" android:layout_width="match_parent" android:layout_height="match_parent"> <com.jarchie.androidui.tiktok.CusVideoView android:id="@+id/mVideoView" android:layout_width="match_parent" android:layout_height="match_parent" android:clickable="false" android:focusable="false" /> <ImageView android:id="@+id/mThumb" android:layout_width="match_parent" android:layout_height="match_parent" android:clickable="false" android:focusable="false" android:scaleType="centerCrop" android:visibility="visible" /> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentEnd="true" android:layout_centerVertical="true" android:layout_marginRight="10dp" android:gravity="center" android:orientation="vertical"> <RelativeLayout android:layout_width="wrap_content" android:layout_height="wrap_content"> <de.hdodenhof.circleimageview.CircleImageView android:layout_width="60dp" android:layout_height="60dp" android:layout_alignParentTop="true" android:src="@drawable/icon_avatar" app:civ_border_color="@android:color/white" app:civ_border_width="2dp" /> <ImageView android:layout_width="20dp" android:layout_height="20dp" android:layout_centerHorizontal="true" android:layout_marginTop="50dp" android:background="@drawable/circle_big_red" android:scaleType="centerInside" android:src="@drawable/add_icon" android:tint="@android:color/white" /> </RelativeLayout> <TextView android:layout_width="50dp" android:layout_height="50dp" android:layout_marginTop="16dp" android:drawableTop="@drawable/heart_icon" android:gravity="center" android:text="8.88w" android:textColor="@android:color/white" /> <TextView android:layout_width="50dp" android:layout_height="50dp" android:layout_marginTop="16dp" android:drawableTop="@drawable/msg_icon" android:gravity="center" android:text="9.99w" android:textColor="@android:color/white" /> <TextView android:layout_width="50dp" android:layout_height="50dp" android:layout_marginTop="16dp" android:drawableTop="@drawable/share_icon" android:gravity="center" android:text="6.66w" android:textColor="@android:color/white" /> </LinearLayout> <de.hdodenhof.circleimageview.CircleImageView android:layout_width="60dp" android:layout_height="60dp" android:layout_alignParentEnd="true" android:layout_alignParentBottom="true" android:layout_marginRight="10dp" android:layout_marginBottom="60dp" android:src="@drawable/header" app:civ_border_color="@color/color_01" app:civ_border_width="12dp" /> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_marginLeft="10dp" android:layout_marginBottom="60dp" android:orientation="vertical"> <TextView android:id="@+id/mTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:lineSpacingExtra="5dp" android:textColor="@android:color/white" android:textSize="16sp" tools:text="测试测试数据哈哈哈哈\n家里几个垃圾了个两个垃圾" /> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:orientation="horizontal" android:gravity="center_vertical"> <ImageView android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/icon_douyin" /> <TextView android:id="@+id/mMarquee" android:layout_width="100dp" android:layout_height="wrap_content" android:layout_marginLeft="10dp" android:textColor="@android:color/white" android:singleLine="true" android:ellipsize="marquee" android:marqueeRepeatLimit="marquee_forever" android:focusable="true" android:focusableInTouchMode="true" android:textSize="14sp"/> </LinearLayout> </LinearLayout> <ImageView android:id="@+id/mPlay" android:layout_width="100dp" android:layout_height="100dp" android:layout_centerInParent="true" android:alpha="0" android:clickable="true" android:focusable="true" android:src="@drawable/play_arrow" /> </RelativeLayout>
1.3、列表Item适配器
然后创建列表Item的适配器TiktokAdapter.java:这里视频封面图片我是自己弄了两张图片,效果看上去不大好,有更好的方案的可以留言一起探讨哦!
public class TiktokAdapter extends RecyclerView.Adapter<TiktokAdapter.ViewHolder> { private int[] videos = {R.raw.v1, R.raw.v2}; private int[] imgs = {R.drawable.fm1, R.drawable.fm2}; private List<String> mTitles = new ArrayList<>(); private List<String> mMarqueeList = new ArrayList<>(); private Context mContext; public TiktokAdapter(Context context) { this.mContext = context; mTitles.add("@乔布奇\nAndroid仿抖音主界面UI效果,\n一起来学习Android开发啊啊啊啊啊\n#Android高级UIAndroid开发"); mTitles.add("@乔布奇\nAndroid RecyclerView自定义\nLayoutManager的使用方式,仿抖音效果哦"); mMarqueeList.add("哈哈创作的原声-乔布奇"); mMarqueeList.add("嘿嘿创作的原声-Jarchie"); } @NonNull @Override public ViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) { return new ViewHolder(LayoutInflater.from(mContext).inflate(R.layout.item_tiktok_layout, viewGroup, false)); } @Override public void onBindViewHolder(@NonNull final ViewHolder holder, int pos) { //第一种方式:获取视频第一帧作为封面图片 // MediaMetadataRetriever media = new MediaMetadataRetriever(); // media.setDataSource(mContext,Uri.parse("android.resource://" + mContext.getPackageName() + "/" + videos[pos % 2])); // holder.mThumb.setImageBitmap(media.getFrameAtTime()); //第二种方式:使用固定图片作为封面图片 holder.mThumb.setImageResource(imgs[pos % 2]); holder.mVideoView.setVideoURI(Uri.parse("android.resource://" + mContext.getPackageName() + "/" + videos[pos % 2])); holder.mTitle.setText(mTitles.get(pos % 2)); holder.mMarquee.setText(mMarqueeList.get(pos % 2)); holder.mMarquee.setSelected(true); } @Override public int getItemCount() { return Integer.MAX_VALUE; } public class ViewHolder extends RecyclerView.ViewHolder { RelativeLayout mRootView; ImageView mThumb; ImageView mPlay; TextView mTitle; TextView mMarquee; CusVideoView mVideoView; public ViewHolder(@NonNull View itemView) { super(itemView); mRootView = itemView.findViewById(R.id.mRootView); mThumb = itemView.findViewById(R.id.mThumb); mPlay = itemView.findViewById(R.id.mPlay); mVideoView = itemView.findViewById(R.id.mVideoView); mTitle = itemView.findViewById(R.id.mTitle); mMarquee = itemView.findViewById(R.id.mMarquee); } } }
二、自定义LayoutManager
我们使用RecyclerView都知道哈,要想让RecylcerView正常工作必须要有两个东西:①、Adapter,负责界面显示适配的,这里我们已经弄好了;②、LayoutManager,告诉列表如何摆放,所以现在想要实现抖音的列表效果的关键就在于这个LayoutManager了,并且普通的LinearLayoutManager是不行的,我们需要自己去实现这个LayoutManger。这里我们取个巧,直接继承LinearLayoutManager来实现自定义布局管理器。
RecyclerView里面有这样一个接口:下面是这个接口的系统源码
//这两个方法不是成对出现的,也没有顺序 public interface OnChildAttachStateChangeListener { void onChildViewAttachedToWindow(@NonNull View var1); void onChildViewDetachedFromWindow(@NonNull View var1); }
它里面有两个方法,可以监听列表的Item添加进来和移除出去的两个动作,这是不是就很符合我们现在的使用场景啊,我们的每一屏只有一个Item,并且要在它被添加进来的时候播放视频,在移除时释放掉,所以我们需要实现这个接口。
需要注意的是,这个接口必须在LayoutManager成功进行初始化之后才能监听,所以我们在LayoutManager中重写onAttachedToWindow()方法,在它里面添加这个接口的监听:
@Override public void onAttachedToWindow(RecyclerView view) { view.addOnChildAttachStateChangeListener(this); super.onAttachedToWindow(view); }
完了之后呢,会重写接口中的两个方法,在这两个方法里面我们就可以来实现播放和暂停的操作了。那么这里问题又来了,播放和暂停这两个动作都涉及到一个问题,你是播放上一个视频还是播放下一个视频,因为列表是可以往下滑也可以往上滑的啊,所以我们还得重写另一个监听位移变化的方法:scrollVerticallyBy(),这里dy的值为正数是往上滑,负数是往下滑
private int mDrift;//位移,用来判断移动方向 @Override public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { this.mDrift = dy; return super.scrollVerticallyBy(dy, recycler, state); }
OK,这样我们就可以判断是向上滑还是向下滑了,那么上面onChildViewAttachedToWindow()这两个方法就可以写了,在这两个方法中,我们需要把具体的业务逻辑回调到Activity里面去处理,所以这里我们还需要再自定义一个接口OnPageSlideListener :
public interface OnPageSlideListener { //释放的监听 void onPageRelease(boolean isNext, int position); //选中的监听以及判断是否滑动到底部 void onPageSelected(int position, boolean isBottom); }
现在来处理上面的两个回调接口onChildViewAttachedToWindow()和onChildViewDetachedFromWindow():
private OnPageSlideListener mOnPageSlideListener; //Item添加进来 @Override public void onChildViewAttachedToWindow(@NonNull View view) { //播放视频操作,判断将要播放的是上一个视频,还是下一个视频 if (mDrift > 0) { //向上 if (mOnPageSlideListener != null) mOnPageSlideListener.onPageSelected(getPosition(view), true); } else { //向下 if (mOnPageSlideListener != null) mOnPageSlideListener.onPageSelected(getPosition(view), false); } } //Item移除出去 @Override public void onChildViewDetachedFromWindow(@NonNull View view) { //暂停播放操作 if (mDrift >= 0) { if (mOnPageSlideListener != null) mOnPageSlideListener.onPageRelease(true, getPosition(view)); } else { if (mOnPageSlideListener != null) mOnPageSlideListener.onPageRelease(false, getPosition(view)); } }
既然这里是通过接口的方式回调到Activity中实现,所以我们还得给它设置一个接口:
//接口注入 public void setOnPageSlideListener(OnPageSlideListener mOnViewPagerListener) { this.mOnPageSlideListener = mOnViewPagerListener; }
写到这里,当然还不行,此时如果你去跑你的项目,你会发现它还是会像普通的RecyclerView一样随意的滑动,所以我们还需要一个类的帮助才行:PagerSnapHelper,它可以实现让RecyclerView像ViewPager一样的滑动效果,这里我们给它绑定上RecyclerView:
private PagerSnapHelper mPagerSnapHelper; public CustomLayoutManager(Context context, int orientation, boolean reverseLayout) { super(context, orientation, reverseLayout); mPagerSnapHelper = new PagerSnapHelper(); } @Override public void onAttachedToWindow(RecyclerView view) { view.addOnChildAttachStateChangeListener(this); mPagerSnapHelper.attachToRecyclerView(view); super.onAttachedToWindow(view); }
推荐阅读:让你明明白白的使用RecyclerView——SnapHelper详解
到这里已经可以实现类似ViewPager滑动的效果了,但是我们还需要重写一个方法,不然的话向下滑动播放的时候会有Bug:因为onChildViewAttachedToWindow()和onChildViewDetachedFromWindow()这两个方法并不是成对出现的,它们二者之间也是没有顺序的,因此这里我们再来监听一下滑动状态的改变:判断已经处理完成即手指抬起时的状态
@Override public void onScrollStateChanged(int state) { switch (state) { case RecyclerView.SCROLL_STATE_IDLE: View view = mPagerSnapHelper.findSnapView(this);//拿到当前进来的View int position = getPosition(view); if (mOnPageSlideListener != null) { mOnPageSlideListener.onPageSelected(position, position == getItemCount() - 1); } break; } }
CustomLayoutManager完整代码如下:
public class CustomLayoutManager extends LinearLayoutManager implements RecyclerView.OnChildAttachStateChangeListener { private int mDrift;//位移,用来判断移动方向 private PagerSnapHelper mPagerSnapHelper; private OnPageSlideListener mOnPageSlideListener; public CustomLayoutManager(Context context) { super(context); } public CustomLayoutManager(Context context, int orientation, boolean reverseLayout) { super(context, orientation, reverseLayout); mPagerSnapHelper = new PagerSnapHelper(); } @Override public void onAttachedToWindow(RecyclerView view) { view.addOnChildAttachStateChangeListener(this); mPagerSnapHelper.attachToRecyclerView(view); super.onAttachedToWindow(view); } //Item添加进来 @Override public void onChildViewAttachedToWindow(@NonNull View view) { //播放视频操作,判断将要播放的是上一个视频,还是下一个视频 if (mDrift > 0) { //向上 if (mOnPageSlideListener != null) mOnPageSlideListener.onPageSelected(getPosition(view), true); } else { //向下 if (mOnPageSlideListener != null) mOnPageSlideListener.onPageSelected(getPosition(view), false); } } //Item移除出去 @Override public void onChildViewDetachedFromWindow(@NonNull View view) { //暂停播放操作 if (mDrift >= 0) { if (mOnPageSlideListener != null) mOnPageSlideListener.onPageRelease(true, getPosition(view)); } else { if (mOnPageSlideListener != null) mOnPageSlideListener.onPageRelease(false, getPosition(view)); } } @Override public void onScrollStateChanged(int state) { //滑动状态监听 switch (state) { case RecyclerView.SCROLL_STATE_IDLE: View view = mPagerSnapHelper.findSnapView(this); int position = getPosition(view); if (mOnPageSlideListener != null) { mOnPageSlideListener.onPageSelected(position, position == getItemCount() - 1); } break; } } @Override public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) { this.mDrift = dy; return super.scrollVerticallyBy(dy, recycler, state); } //接口注入 public void setOnPageSlideListener(OnPageSlideListener mOnViewPagerListener) { this.mOnPageSlideListener = mOnViewPagerListener; } }
三、实现播放
我们接着在Activity中实现播放和停止的方法:
//播放 private void playVideo() { View itemView = mRecycler.getChildAt(0); final CusVideoView mVideoView = itemView.findViewById(R.id.mVideoView); final ImageView mPlay = itemView.findViewById(R.id.mPlay); final ImageView mThumb = itemView.findViewById(R.id.mThumb); final MediaPlayer[] mMediaPlayer = new MediaPlayer[1]; mVideoView.start(); mVideoView.setOnInfoListener(new MediaPlayer.OnInfoListener() { @Override public boolean onInfo(MediaPlayer mp, int what, int extra) { mMediaPlayer[0] = mp; mp.setLooping(true); mThumb.animate().alpha(0).setDuration(200).start(); return false; } }); //暂停控制 mPlay.setOnClickListener(new View.OnClickListener() { boolean isPlaying = true; @Override public void onClick(View v) { if (mVideoView.isPlaying()) { mPlay.animate().alpha(1f).start(); mVideoView.pause(); isPlaying = false; } else { mPlay.animate().alpha(0f).start(); mVideoView.start(); isPlaying = true; } } }); } //释放 private void releaseVideo(int index) { View itemView = mRecycler.getChildAt(index); final CusVideoView mVideoView = itemView.findViewById(R.id.mVideoView); final ImageView mThumb = itemView.findViewById(R.id.mThumb); final ImageView mPlay = itemView.findViewById(R.id.mPlay); mVideoView.stopPlayback(); mThumb.animate().alpha(1).start(); mPlay.animate().alpha(0f).start(); }
然后处理LayoutManager中回调到Activity中的播放逻辑:
mLayoutManager.setOnPageSlideListener(new OnPageSlideListener() { @Override public void onPageRelease(boolean isNext, int position) { int index; if (isNext) { index = 0; } else { index = 1; } releaseVideo(index); } @Override public void onPageSelected(int position, boolean isNext) { playVideo(); } });
Activity的完整代码如下:
public class TikTokIndexActivity extends AppCompatActivity { private static final String TAG = TikTokIndexActivity.class.getSimpleName(); private RecyclerView mRecycler; private CustomLayoutManager mLayoutManager; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_tiktok_layout); initView(); initListener(); } //初始化监听 private void initListener() { mLayoutManager.setOnPageSlideListener(new OnPageSlideListener() { @Override public void onPageRelease(boolean isNext, int position) { int index; if (isNext) { index = 0; } else { index = 1; } releaseVideo(index); } @Override public void onPageSelected(int position, boolean isNext) { playVideo(); } }); } //初始化View private void initView() { mRecycler = findViewById(R.id.mRecycler); mLayoutManager = new CustomLayoutManager(this, OrientationHelper.VERTICAL, false); TiktokAdapter mAdapter = new TiktokAdapter(this); mRecycler.setLayoutManager(mLayoutManager); mRecycler.setAdapter(mAdapter); } //播放 private void playVideo() { //...这里的代码见上方说明 } //释放 private void releaseVideo(int index) { //...这里的代码见上方说明 } }
到这里,仿抖音首页播放的效果就简单实现了,OK,咱们下期再会吧!
祝:工作顺利!
到此这篇关于Android仿抖音主页效果实现的文章就介绍到这了,更多相关Android抖音主页内容请搜索海外IDC网以前的文章或继续浏览下面的相关文章希望大家以后多多支持海外IDC网!
【文章来自:http://www.yidunidc.com/gfcdn.html 欢迎留下您的宝贵建议】