android.graphics.Movie

本文介绍了一种在Android中使用自定义GIFView组件显示GIF动画的方法,通过继承ImageView并利用android.graphics.Movie类实现了动画效果。

如何在Android中显示GIF动画,有很多方法,比如可以使用J2ME平台上那个解码工具类,纯java的,拿来即可。
但是其实Android还是为我们提供了一个更为方便的工具:android.graphics.Movie。

参考例子在ApiDemos中的BitmapDecode中。

下面我只是简单地用它来实现一个自己的GIFView,以方便在各种需要使用GIF动画的场合使用。

为了简单,我让GIFView extends ImageView罢了。它在布局中的描述如下:

  1. <cn.sharetop.android.view.GIFView  
  2.     android:id="@+id/gif"  
  3.     android:layout_gravity="center_horizontal"  
  4.     android:layout_width="278px"  
  5.     android:layout_height="183px"  
  6.     android:scaleType="fitXY"  
  7.     app:gif="@drawable/a"  
  8.     android:src="@drawable/a"  
  9.     />  


与ImageView唯一的区别在于我加了一个gif属性,与src属性的值是一样的。不过它们需要同时存在,不可省略其中之一(后面我会说明为什么)。

注意因为gif属性,所以别忘了那个attr.xml中也要加上:
  1. <resources>  
  2.     <declare-styleable name="GIFView">  
  3.         <attr name="gif" format="reference"  />  
  4.     </declare-styleable>  
  5. </resources>  



然后是代码,没几行的:

  1. public class GIFView extends ImageView {  
  2.     private static final String TAG="GIFView";  
  3.      
  4.     private Movie mMovie;     
  5.     private long mMovieStart;  
  6.      
  7.      
  8.     //此处省略几个构造函数  
  9.     //......  
  10.     //主要的构造函数  
  11.     public GIFView(Context context, AttributeSet attrs, int defStyle) {  
  12.         super(context, attrs, defStyle);  
  13.         // TODO Auto-generated constructor stub  
  14.          
  15.         mMovie=null;  
  16.         mMovieStart=0;  
  17.          
  18.         //从描述文件中读出gif的值,创建出Movie实例  
  19.         TypedArray a = context.obtainStyledAttributes(attrs,  
  20.                 R.styleable.GIFView, defStyle, 0);  
  21.          
  22.         int srcID=a.getResourceId(R.styleable.GIFView_gif, 0);  
  23.         if(srcID>0){  
  24.             InputStream is = context.getResources().openRawResource(srcID);  
  25.             mMovie = Movie.decodeStream(is);  
  26.         }  
  27.          
  28.         a.recycle();  
  29.     }  
  30.     //主要的工作是重载onDraw  
  31.     @Override  
  32.     protected void onDraw(Canvas canvas) {  
  33.         // TODO Auto-generated method stub  
  34.         //super.onDraw(canvas);  
  35.          
  36.         //当前时间  
  37.         long now = android.os.SystemClock.uptimeMillis();  
  38.         //如果第一帧,记录起始时间  
  39.         if (mMovieStart == 0) {   // first time  
  40.               mMovieStart = now;  
  41.         }  
  42.         if (mMovie != null) {  
  43.                   //取出动画的时长  
  44.             int dur = mMovie.duration();  
  45.                   if (dur == 0) {  
  46.                       dur = 1000;  
  47.                   }  
  48.                   //算出需要显示第几帧  
  49.             int relTime = (int)((now - mMovieStart) % dur);  
  50.            
  51.                   //Log.d(TAG,"---onDraw..."+mMovie.toString()+",,,,"+relTime);  
  52.              //设置要显示的帧,绘制即可  
  53.                   mMovie.setTime(relTime);  
  54.             mMovie.draw(canvas,0,0);  
  55.                   invalidate();  
  56.         }         
  57.     }  
  58.          
  59. }  



代码中已有注释,就不多说了。我的理解是Movie其实管理着GIF动画中的多个帧,只需要通过 setTime() 一下就可以让它在draw()的时候绘出相应的那帧图像。
通过当前时间与duration之间的换算关系,是很容易实现GIF动起来的效果。


最后,说一下为什么src与gif要同时存在了,因为我这个GIFView很简单,没有自己去onMeasure,所以要借助src让ImageView去计算它的尺寸和布局之类的事情。
只是在onDraw的时候,不显示src而已。

如果感兴趣的同学可以自己完善这个GIFView,比如以下两点:
1. 只需要一个gif属性,不要src了,或者直接使用src属性?
2. 如果在xml中没有指定gif/src的值,增加一些方法让用户可以通过代码设置gif和src的值

[补充]

刚才又觉得这段代码有修正的必要:

1. 关于如何直接使用src这个属性,仍是修改attr.xml中,这样即可:

  1. <resources>  
  2.     <declare-styleable name="GIFView">  
  3.         <attr name="android:src" />  
  4.     </declare-styleable>  
  5. </resources>  

然后在main.xml中就不再需要gif这个属性,直接用src就可以了。


package com.lss.loginregister.adapter; import android.annotation.SuppressLint; import android.content.Context; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.ListAdapter; import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.lss.loginregister.R; import com.lss.loginregister.databinding.ItemMovieBinding; import com.lss.loginregister.entity.Movie; public class MovieAdapter extends ListAdapter<Movie, MovieAdapter.MovieViewHolder> { private static final String TAG = "MovieAdapter"; private final Context context; // 使用DiffUtil优化列表更新 private static final DiffUtil.ItemCallback<Movie> DIFF_CALLBACK = new DiffUtil.ItemCallback<Movie>() { @Override public boolean areItemsTheSame(@NonNull Movie oldItem, @NonNull Movie newItem) { return oldItem.getTitle().equals(newItem.getTitle()); // 用唯一标识判断 } @SuppressLint("DiffUtilEquals") @Override public boolean areContentsTheSame(@NonNull Movie oldItem, @NonNull Movie newItem) { return oldItem.equals(newItem); } }; public MovieAdapter(Context context) { super(DIFF_CALLBACK); this.context = context; } @NonNull @Override public MovieViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { // 使用DataBinding绑定列表项 ItemMovieBinding binding = ItemMovieBinding.inflate( LayoutInflater.from(parent.getContext()), parent, false ); return new MovieViewHolder(binding); } @Override public void onBindViewHolder(@NonNull MovieViewHolder holder, int position) { Movie movie = getItem(position); // 强制清除之前的绑定数据,避免复用冲突 holder.binding.tvTitle.setText(null); holder.binding.tvQuote.setText(null); holder.binding.tvRating.setText(null); Log.i(TAG, "onBindViewHolder: position" + position); Log.i(TAG, "onBindViewHolder: " + movie.toString()); // 重新绑定新数据 holder.binding.tvTitle.setText(movie.getTitle()); holder.binding.tvQuote.setText(movie.getQuote() != null ? movie.getQuote() : ""); holder.binding.tvRating.setText(String.format("评分: %.1f", movie.getRating())); // 确保视图可见 holder.binding.tvTitle.setVisibility(View.VISIBLE); holder.binding.tvQuote.setVisibility(View.VISIBLE); holder.binding.tvRating.setVisibility(View.VISIBLE); // 强制执行绑定,确保立即更新UI holder.binding.executePendingBindings(); // 图片加载放在最后,避免阻塞文本渲染 Glide.with(holder.binding.getRoot().getContext()) .load(movie.getImageUrl()) .placeholder(R.drawable.placeholder_poster) .error(R.drawable.error_poster) .diskCacheStrategy(DiskCacheStrategy.ALL) .into(holder.binding.ivPoster); } // ViewHolder通过DataBinding绑定数据 public static class MovieViewHolder extends RecyclerView.ViewHolder { private final ItemMovieBinding binding; public MovieViewHolder(ItemMovieBinding binding) { super(binding.getRoot()); this.binding = binding; // 确保视图在创建时就初始化 binding.tvTitle.setVisibility(View.VISIBLE); binding.tvQuote.setVisibility(View.VISIBLE); } public void bind(Movie movie) { binding.executePendingBindings(); // 强制执行绑定 } } } 为什么没有new List 在adapter package com.lss.loginregister.activity; import android.os.Bundle; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import androidx.activity.EdgeToEdge; import androidx.appcompat.app.AppCompatActivity; import androidx.core.graphics.Insets; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsCompat; import androidx.lifecycle.ViewModelProvider; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import com.google.android.material.chip.Chip; import com.lss.loginregister.R; import com.lss.loginregister.adapter.MovieAdapter; import com.lss.loginregister.databinding.ActivityMovieListBinding; import com.lss.loginregister.viewmodel.MovieListViewModel; public class MovieListActivity extends AppCompatActivity { private static final String TAG = "MovieListActivity"; private ActivityMovieListBinding binding; private MovieListViewModel viewModel; private MovieAdapter adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); EdgeToEdge.enable(this); binding = ActivityMovieListBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); // 设置工具栏 setSupportActionBar(binding.toolbar); if (getSupportActionBar() != null) { Log.i(TAG, "onCreate: display toolbar"); getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayShowHomeEnabled(true); } // 初始化适配器 adapter = new MovieAdapter(this); binding.recyclerView.setLayoutManager(new LinearLayoutManager(this)); binding.recyclerView.setAdapter(adapter); // 初始化ViewModel viewModel = new ViewModelProvider(this).get(MovieListViewModel.class); // 绑定ViewModel到布局 binding.setViewModel(viewModel); binding.setLifecycleOwner(this); // 设置下拉刷新 binding.swipeRefresh.setOnRefreshListener(() -> { viewModel.refreshData(); binding.swipeRefresh.setRefreshing(false); }); // 设置排序Chip点击事件 setupSortChips(); // 观察电影列表数据变化 viewModel.movieList.observe(this, movies -> { if (movies != null && !movies.isEmpty()) { adapter.submitList(movies, () -> { binding.recyclerView.scrollToPosition(0); }); binding.emptyTextView.setVisibility(View.GONE); } else { binding.emptyTextView.setVisibility(View.VISIBLE); } }); // 观察加载状态 viewModel.isLoading.observe(this, isLoading -> { binding.progressBar.setVisibility(isLoading ? View.VISIBLE : View.GONE); if (isLoading) { binding.errorTextView.setVisibility(View.GONE); } }); // 观察错误信息 viewModel.errorMsg.observe(this, errorMsg -> { if (errorMsg != null && !errorMsg.isEmpty()) { binding.errorTextView.setText(errorMsg); binding.errorTextView.setVisibility(View.VISIBLE); binding.emptyTextView.setVisibility(View.GONE); } else { binding.errorTextView.setVisibility(View.GONE); } }); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.menu_movie_list, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { int id = item.getItemId(); if (id == android.R.id.home) { onBackPressed(); return true; } else if (id == R.id.action_sort_default) { viewModel.sortMovies(MovieListViewModel.SortType.DEFAULT); setSelectedChip(binding.chipDefault); return true; } else if (id == R.id.action_sort_rating) { viewModel.sortMovies(MovieListViewModel.SortType.RATING); setSelectedChip(binding.chipRating); return true; } else if (id == R.id.action_sort_title) { viewModel.sortMovies(MovieListViewModel.SortType.TITLE); setSelectedChip(binding.chipTitle); return true; } return super.onOptionsItemSelected(item); } /** * 设置排序Chip点击事件 */ private void setupSortChips() { binding.chipDefault.setOnClickListener(v -> { viewModel.sortMovies(MovieListViewModel.SortType.DEFAULT); setSelectedChip(binding.chipDefault); }); binding.chipRating.setOnClickListener(v -> { viewModel.sortMovies(MovieListViewModel.SortType.RATING); setSelectedChip(binding.chipRating); }); binding.chipTitle.setOnClickListener(v -> { viewModel.sortMovies(MovieListViewModel.SortType.TITLE); setSelectedChip(binding.chipTitle); }); // 设置初始选中状态 setSelectedChipBasedOnSortType(); } /** * 根据当前排序类型设置选中的Chip */ private void setSelectedChipBasedOnSortType() { switch (viewModel.getCurrentSortType()) { case DEFAULT: setSelectedChip(binding.chipDefault); break; case RATING: setSelectedChip(binding.chipRating); break; case TITLE: setSelectedChip(binding.chipTitle); break; } } /** * 设置选中的Chip * * @param chip 要选中的Chip */ private void setSelectedChip(Chip chip) { binding.chipDefault.setChecked(chip == binding.chipDefault); binding.chipRating.setChecked(chip == binding.chipRating); binding.chipTitle.setChecked(chip == binding.chipTitle); } } <?xml version="1.0" encoding="utf-8"?> <layout 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"> <data> <import type="android.view.View" /> <variable name="viewModel" type="com.lss.loginregister.viewmodel.MovieListViewModel" /> </data> <androidx.swiperefreshlayout.widget.SwipeRefreshLayout android:id="@+id/swipe_refresh" android:layout_width="match_parent" android:layout_height="match_parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/toolbar"> <androidx.constraintlayout.widget.ConstraintLayout android:layout_width="match_parent" android:layout_height="match_parent" android:background="#F5F5F5"> <!-- 顶部工具栏 --> <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:layout_marginTop="15dp" android:background="@color/tool_white" app:layout_constraintTop_toTopOf="parent" app:menu="@menu/menu_movie_list" app:title="@string/top20" app:titleTextAppearance="@style/ToolbarTitleTextAppearance" app:titleTextColor="@color/_1e88e5" /> <!-- 添加菜单 --> <!-- 排序选项栏 --> <com.google.android.material.chip.ChipGroup android:id="@+id/sortChipGroup" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:paddingHorizontal="16dp" android:visibility="@{viewModel.movieList != null && viewModel.movieList.size() > 0 ? View.VISIBLE : View.GONE}" app:chipSpacing="8dp" app:layout_constraintTop_toBottomOf="@id/toolbar" app:singleSelection="true"> <com.google.android.material.chip.Chip android:id="@+id/chipDefault" style="@style/Widget.MaterialComponents.Chip.Choice" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/default_sort" app:checkedIconVisible="false" app:chipBackgroundColor="@color/chip_background_selector" /> <com.google.android.material.chip.Chip android:id="@+id/chipRating" style="@style/Widget.MaterialComponents.Chip.Choice" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/rating_sort" app:checkedIconVisible="false" app:chipBackgroundColor="@color/chip_background_selector" /> <com.google.android.material.chip.Chip android:id="@+id/chipTitle" style="@style/Widget.MaterialComponents.Chip.Choice" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/title_sort" app:checkedIconVisible="false" app:chipBackgroundColor="@color/chip_background_selector" /> </com.google.android.material.chip.ChipGroup> <!-- 电影列表 --> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="match_parent" android:layout_height="0dp" android:padding="8dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toBottomOf="@id/sortChipGroup" tools:listitem="@layout/item_movie" /> <!-- 加载指示器 --> <ProgressBar android:id="@+id/progressBar" android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <!-- 错误信息 --> <TextView android:id="@+id/errorTextView" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:padding="16dp" android:text="@{viewModel.errorMsg}" android:textColor="#F44336" android:visibility="@{viewModel.errorMsg != null ? View.VISIBLE : View.GONE}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <!-- 空数据提示 --> <TextView android:id="@+id/emptyTextView" android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" android:padding="16dp" android:text="@string/null_info" android:textColor="#9E9E9E" android:visibility="@{viewModel.movieList == null || viewModel.movieList.size() == 0 ? View.VISIBLE : View.GONE}" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </layout>
最新发布
08-02
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值