Android仿抖音主頁效果實現程式碼
寫在前面
各位老鐵,我又來啦!既然來了,那肯定又來搞事情啦,話不多說,先上圖!
“抖音”都玩過吧,是不是很好玩,我反正是天天刷,作為一個非著名的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,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),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.State state) { this.mDrift = dy; return super.scrollVerticallyBy(dy,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,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抖音主頁內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!