【Android-Kotlin-Volley】图片画廊学习笔记

下载地址:

  1. APP下载地址: https://wws.lanzous.com/b01tsdd7c 密码:dp1f
  2. Github: https://github.com/kirikaTowa/GalleryVolley

Boat

  1. 视频来源:https://www.bilibili.com/video/BV1sJ41127EMlongway777
  2. Demo学习:Http库,Volley
  3. 图片加载库: Glide
  4. 动态占位符效果:Shimmerlayout
  5. 下拉刷新工具:SwipeRefreshLayout
  6. 设置RecycleView实现每行两个的布局在这里插入图片描述
  7. Pixabay 开放API:
  8. 注意序列化的命名一定要对上,由于是自动赋值,名字对不上就拿不到数据。
https://pixabay.com/api/docs/

【Android-Kotlin-Volley】图片画廊学习笔记_第1张图片
【Android-Kotlin-Volley】图片画廊学习笔记_第2张图片

1. 测试Volley架构

2.添加下拉刷新

  1. SwipeRefreshLayout包裹内容即可,目前版本安卓自带,想导库也可以

implementation ‘androidx.swiperefreshlayout:swiperefreshlayout:1.0.0’

  1. 主界面操作
    1)找到并声明控件
    2)设置监听器,内部设置一个监听对象,拉动即可进入加载状态(抽取glide加载图片方法,完成后停止刷新)

public class MainActivity extends AppCompatActivity {
    String url1="https://images.pexels.com/photos/240040/pexels-photo-240040.jpeg?auto=compress&cs=tinysrgb&h=650&w=940";
    String url2="https://images.pexels.com/photos/459225/pexels-photo-459225.jpeg?auto=compress&cs=tinysrgb&dpr=3&h=750&w=1260";
    private ImageView imageView;
    private  SwipeRefreshLayout swipeRefreshLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
         swipeRefreshLayout=findViewById(R.id.swipeRefreshLayout);
         imageView= findViewById(R.id.imageView)
         

        RequestQueue mQueue = Volley.newRequestQueue(this);
        swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
            @Override
            public void onRefresh() {
                loadImage();
            }
        });
    }

    void loadImage() {
        Random random=new Random();
        String url=random.nextBoolean()? url1 :url2;//两张图几率5 5 开 该方法产生一个均匀分布的bool值
        Glide.with(this)
                .load(url)
                .placeholder(R.drawable.ic_launcher_background)//占位符
                .listener(new RequestListener<Drawable>() {
                    @Override
                    public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) {
                        if (swipeRefreshLayout.isRefreshing())
                            swipeRefreshLayout.setRefreshing(false);
                        return false;
                    }

                    @Override
                    public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) {
                        if (swipeRefreshLayout.isRefreshing())
                            swipeRefreshLayout.setRefreshing(false);
                        return false;
                    }
                })
                .into(imageView);
    }
}

3. ViewModel测试

4. 正式开启画廊

4.1. 导包

    implementation 'androidx.navigation:navigation-fragment-ktx:2.1.0'
    implementation 'androidx.navigation:navigation-ui-ktx:2.1.0'
    implementation 'com.google.code.gson:gson:2.2.4'
    implementation 'com.android.volley:volley:1.1.1'
    implementation 'io.supercharge:shimmerlayout:2.1.0'//闪动占位符
    implementation 'com.github.bumptech.glide:glide:4.10.0'
    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0-beta01'
    implementation 'com.github.chrisbanes.photoview:library:1.2.4'//支持手势缩放

  1. 使用Pixabay API

4.2 代码

1.布局

  1. 布局建两个Fragment(基础blank类型),画册页Fragment和点开大图Fragment
class GalleryFragment : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_gallery, container, false)
    }
}
  1. 建立一个navigation导航,用于连接两个Fragment的跳转
    1)。(res->new ->android resource file type选择navigation),点击添加两个Fragment
    【Android-Kotlin-Volley】图片画廊学习笔记_第3张图片
    2)连接导航线路
    【Android-Kotlin-Volley】图片画廊学习笔记_第4张图片
    3)来到MainAC的layout,把我们建立的navigation拖进来 【Android-Kotlin-Volley】图片画廊学习笔记_第5张图片

2. 观察API

  1. 在FarawayPlayer中我们知道,真实数据进行了两层包裹,先将外层全定义接收,其中一个用List进行二层的多级包裹,再创建第二个类。选取所需的字段
    【Android-Kotlin-Volley】图片画廊学习笔记_第6张图片
  2. 建立Pixabay类对应API
data class Pixabay(//自动添加域和set get
    val totalHits:Int,
    val hits:Array<PhotoItem>,
    val total:Int
)

data class PhotoItem(
    val webformatURL:String,
    val id:Int,
    val largeImageURL:String

)
  1. 可以定制序列化接口,1)可以实现后续传递 2)平衡API元组和自定义属性。
data class Pixabay(//自动添加域和set get
    val totalHits:Int,
    val hits:Array<PhotoItem>,
    val total:Int
) {
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (javaClass != other?.javaClass) return false

        other as Pixbay

        if (totalHits != other.totalHits) return false
        if (!hits.contentEquals(other.hits)) return false
        if (total != other.total) return false

        return true
    }

    override fun hashCode(): Int {
        var result = totalHits
        result = 31 * result + hits.contentHashCode()
        result = 31 * result + total
        return result
    }
}

data class PhotoItem(
    @SerializedName("webformatURL")val previewUrl:String,
    @SerializedName("id")val photoId:Int,
    @SerializedName("largeImageURL")val fullUrl:String

)

3. 建立第一个页面的GalleryViewModel

  1. 实现ViewModel和建立LiveData
    【Android-Kotlin-Volley】图片画廊学习笔记_第7张图片
  2. 建立Volley单例模式:VolleySingleton
class VolleySingleton private constructor(context:Context){
    companion object {
        private var INSTANCE : VolleySingleton?=null
        fun getInstance(context: Context) =
            INSTANCE?: synchronized(this) {//避免多线程碰撞
                VolleySingleton(context).also { INSTANCE = it }
            }

    }

    val requestQueue:RequestQueue by lazy {
        Volley.newRequestQueue(context.applicationContext)
    }
}
  1. Volley先建立请求,再发送通知
class GalleryViewModel(application: Application) : AndroidViewModel(application) {
    //私有化vitable,开发非 Mutable
    private val _photoListLive= MutableLiveData<List<PhotoItem>>()
    //开放
    val photoListLive:LiveData<List<PhotoItem>>
    get() = _photoListLive//只能读取而不能进行数据源的更改

    fun fetchData(){
        val stringResult=StringRequest(
            Request.Method.GET,
            getUrl(),
            //解析后赋值给内容
            Response.Listener {//正确与失败后的监听器
//                _photoListLive.value=Gson().fromJson(it,Pixabay::class.java).hits.toList()
            },
            Response.ErrorListener {
                Log.d("hello",it.toString())
            }
        )//Request导Volley包
        //添加volley队列
        VolleySingleton.getInstance(getApplication()).requestQueue.add(stringResult)//加载 reponse会回调
    }



    private fun getUrl():String {
        return "https://pixabay.com/api/?key=12472743-874dc01dadd26dc44e0801d61&q=${keyWords.random()}&per_page=100"
    }
    //关键词随机化处理
    private val keyWords = arrayOf("cat", "dog", "panda", "beauty", "miku", "animal")
}

4. 回到第一个Fragment界面操作加入RecycleView

  1. 将其转为swiperefreshlayout,刷新
    【Android-Kotlin-Volley】图片画廊学习笔记_第8张图片
  2. 加入recycleview与对应条目,gallery_cell条目对应发光体和包裹图片
    在这里插入图片描述
    【Android-Kotlin-Volley】图片画廊学习笔记_第9张图片
  3. 建立adapter->这里我们使用更简洁的ListAdapter,实现两个方法后发现的仍然报错-》新特性:需要一个比较器
    【Android-Kotlin-Volley】图片画廊学习笔记_第10张图片
    【Android-Kotlin-Volley】图片画廊学习笔记_第11张图片
    在这里插入图片描述
  4. 完成onCreateViewHolder,onBindViewHolder

class GalleryAdapter : ListAdapter<PhotoItem, GalleryAdapter.MyViewHolder>(DIFFCALLBACK) {

    //初始化
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        //创建holder
        val holder = MyViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.gallery_cell, parent, false))
        holder.itemView.setOnClickListener {
            /* Bundle().apply {
                 putParcelable("PHOTO",getItem(holder.adapterPosition))
                 holder.itemView.findNavController().navigate(R.id.action_galleryFragment_to_photoFragment,this)
             }*/
        }

        return holder
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        holder.itemView.shimmerLayoutCell.apply {
          setShimmerColor(0x55FFFFFF)
          setShimmerAngle(0)//闪动角度
          startShimmerAnimation()
        }

        Glide.with(holder.itemView)
            .load(getItem(position).previewUrl)//photo对象
            .placeholder(R.drawable.ic_photo_gray_24dp)
            .listener(object : RequestListener<Drawable> {
                override fun onResourceReady(
                    resource: Drawable?,
                    model: Any?,
                    target: Target<Drawable>?,
                    dataSource: DataSource?,
                    isFirstResource: Boolean
                ): Boolean {
                    return false.also { holder.itemView.shimmerLayoutCell?.stopShimmerAnimation() }//判断空, 图片未加载完全就切走 但listenrer还在监听
                }

                override fun onLoadFailed(
                    e: GlideException?,
                    model: Any?,
                    target: com.bumptech.glide.request.target.Target<Drawable>?,
                    isFirstResource: Boolean
                ): Boolean {
                    return false//都return false 不然不显图
                }//监听加载完成后停止shimmer
            }
            )
            .into(holder.itemView.imageView)//加载上去
    }

    object DIFFCALLBACK : DiffUtil.ItemCallback<PhotoItem>() {
        override fun areItemsTheSame(oldItem: PhotoItem, newItem: PhotoItem): Boolean {
            return oldItem === newItem //判断是否为同一个对象 三等于号
        }

        override fun areContentsTheSame(oldItem: PhotoItem, newItem: PhotoItem): Boolean {
            return oldItem.photoId == newItem.photoId//判断内容是否相同

        }
    }

    class MyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

}

添加占位符
【Android-Kotlin-Volley】图片画廊学习笔记_第12张图片
【Android-Kotlin-Volley】图片画廊学习笔记_第13张图片
绑定具体的List操作在submitlist实现,见下面
5. 来到GalleryFragment做整合
1)先将RecycleView进行初始化
2)以观察者方式观察list,若该lisr为空则先加载数据

class GalleryFragment : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_gallery, container, false)
    }


    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        val galleryAdapter=GalleryAdapter()

        recycleView.apply{
            adapter=galleryAdapter
            layoutManager= GridLayoutManager(requireContext(),2)//两列
        }


        //创建一个观察
        val galleryViewModel= ViewModelProvider(this).get(GalleryViewModel::class.java)
        //创建一个观察 提交装配一下
        galleryViewModel.photoListLive.observe(this, Observer {
            galleryAdapter.submitList(it)//listadapter submit即可更新数据
            //接收到数据就关闭刷新栏
            // swipeLayoutGallery.isRefreshing = false
        })
        //如果是空的 直接加载内容 不用手动拉一下
        galleryViewModel.photoListLive.value?:galleryViewModel.fetchData()
        //设置下拉获取数据

    }
}
  1. 添加权限
 <uses-permission android:name="android.permission.INTERNET"/>
  1. 运行报错,Viewmodel初始化问题,因为这次继承的是AndroidViewModel,声明需factory,且有参数
  //创建一个观察
        val galleryViewModel=
            ViewModelProvider(this,ViewModelProvider.AndroidViewModelFactory(requireActivity().application)).get(GalleryViewModel::class.java)

  1. 增加下拉刷新和加载成功后消失,Viewmodel就不用写接口了
    【Android-Kotlin-Volley】图片画廊学习笔记_第14张图片
  2. 加入右上角menu
    1)增设menu文件
    【Android-Kotlin-Volley】图片画廊学习笔记_第15张图片
    2)GalleryFragment复写Menu方法
    3)onActivityCreated中设置占位为true
    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        super.onCreateOptionsMenu(menu, inflater)
        inflater.inflate(R.menu.menu,menu)
    }

【Android-Kotlin-Volley】图片画廊学习笔记_第16张图片

  1. 设置menu监听事件,这里需要viewmodel,就把其放到最外面惰性加载
   private lateinit var galleryViewModel: GalleryViewModel
    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when(item.itemId){
            R.id.swipeIndicator -> {
                swiperefreshlayout.isRefreshing = true
                galleryViewModel.fetchData()
            }

        }

        return super.onOptionsItemSelected(item)
    }
  1. 发现转的很快就消失了,界面不是很友好,利用handler进行延迟加载

在这里插入图片描述

5. 处理两个Fragment的跳转

  1. 回到adapter的点击事件,利用Bundle和Parcelabel接口(序列化)传递数据。之前的FarawayPlay是利用Intent进行传递的,且点击事件是在Activity中。
    1)使用apply将我们所需要的序列化数据put进我们定义的名字中,利用导航控制器确定跳转及传递Bundle。
    2)若有switch树形的跳转navigation,则我们可以多些几个导航跳转,然后设置不同的导航控制器

  2. 之前实现序列化接口都要实现其对应方法;build.gradle中加入experimental=true;
    增加@Parcelize标注即可;

【Android-Kotlin-Volley】图片画廊学习笔记_第17张图片

【Android-Kotlin-Volley】图片画廊学习笔记_第18张图片
【Android-Kotlin-Volley】图片画廊学习笔记_第19张图片
【Android-Kotlin-Volley】图片画廊学习笔记_第20张图片

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        //创建holder
        val holder = MyViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.gallery_cell, parent, false))
        holder.itemView.setOnClickListener {
            Bundle().apply {
                 putParcelable("PHOTO",getItem(holder.adapterPosition))
                 holder.itemView.findNavController().navigate(R.id.action_galleryFragment_to_photoFragment,this)//this指创建的Bundle
             }
        }

        return holder
    }
  1. 修该fragment_photo的界面
    【Android-Kotlin-Volley】图片画廊学习笔记_第21张图片
  2. 修改代码
    1)通过arguments在Glide进行加载是获取Bundle中设定的键值对取出positon并获取图片URL
class PhotoFragment : Fragment() {
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_photo, container, false)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        shimmerLayoutphoto.apply {
            setShimmerColor(0x55FFFFFF)
            setShimmerAngle(0)//闪动角度
            startShimmerAnimation()
        }

        Glide.with(requireActivity())
            .load(arguments?.getParcelable<PhotoItem>("PHOTO")?.fullUrl)//获取网址
            .placeholder(R.drawable.ic_photo_gray_24dp)//设定占位符
            .listener(object :RequestListener<Drawable>{//监听load的情况
                override fun onLoadFailed(
                    e: GlideException?,
                    model: Any?,
                    target: Target<Drawable>?,
                    isFirstResource: Boolean
                ): Boolean {
                    return false
                }

                override fun onResourceReady(
                    resource: Drawable?,
                    model: Any?,
                    target: Target<Drawable>?,
                    dataSource: DataSource?,
                    isFirstResource: Boolean
                ): Boolean {
                    return false.also { shimmerLayoutphoto.stopShimmerAnimation() }
                }

            })


            .into(photoView)
    }


  1. 完善Navigation导航
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        NavigationUI.setupActionBarWithNavController(this,findNavController(R.id.fragment))
    }

    override fun onSupportNavigateUp(): Boolean {
        return super.onSupportNavigateUp() || findNavController(R.id.fragment).navigateUp()
    }
}

4.3 新特性,加入图片浏览的左右滑动以及图片下载

  1. 删除photo及相关的xml文件【delete anyway】,注意navigation中的fragment引用不会自动消失,所以要进入进行删除
  2. 建立新的第二个fragment文件,界面上调整gravity进行位移至最下面
    【Android-Kotlin-Volley】图片画廊学习笔记_第22张图片
    【Android-Kotlin-Volley】图片画廊学习笔记_第23张图片
  3. 目前该界面只是有个骨架,没有填充东西,所以新建xml,时候用viewpager适配
    【Android-Kotlin-Volley】图片画廊学习笔记_第24张图片
  4. 建立adapterPager->PhotoListAdapter

class PagerPhotoListAdapter: ListAdapter<PhotoItem, PagerPhotoListAdapter.PagerPhotoViewHolder>(DiffCallBack) {
    object DiffCallBack : DiffUtil.ItemCallback<PhotoItem>() {
        override fun areItemsTheSame(oldItem: PhotoItem, newItem: PhotoItem): Boolean {
            return oldItem === newItem
        }

        override fun areContentsTheSame(oldItem: PhotoItem, newItem: PhotoItem): Boolean {
            return oldItem.photoId == newItem.photoId
        }
    }
    class PagerPhotoViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PagerPhotoViewHolder {
        LayoutInflater.from(parent.context).inflate(R.layout.pager_photo_view, parent, false)
            .apply {
                return PagerPhotoViewHolder(this)
            }
    }

    override fun onBindViewHolder(holder: PagerPhotoViewHolder, position: Int) {
        Glide.with(holder.itemView)
            .load(getItem(position).previewUrl)
            .placeholder(R.drawable.ic_photo_gray_24dp)
            .into(holder.itemView.pagerPhoto)
    }

}
  1. 来到PagerPhotoFragment,设置数据源,需要从第一个Fragment绑定的数据传递过来
    【Android-Kotlin-Volley】图片画廊学习笔记_第25张图片
  2. 来到第一个Fragment对应adapter的点击事件,原fragment删了,这边跳转肯定出问题,原本传递一个Position来找对应图片,而这次还要将整个List全传递过来,并且将新Fragment加入navigation引导。
    【Android-Kotlin-Volley】图片画廊学习笔记_第26张图片
    【Android-Kotlin-Volley】图片画廊学习笔记_第27张图片
  3. 修改对应fragment,将数据源设定给Viewpageradapter,取出list,更新list
class PagerPhotoFragment : Fragment() {



    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_pager_photo, container, false)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        val phootList: ArrayList<PhotoItem>? =arguments?.getParcelableArrayList<PhotoItem>("PHOTO_LIST")
        PagerPhotoListAdapter().apply {
            viewPager2.adapter=this//二代viewpager可接受listadapter
            //再给个数据
            submitList(phootList)
        }
    }


}

即可实现左右滑动了

  1. 下面没有同步变化页面,所以加入viewpager监听,但有个监听机制问题,每次这个position都从0开始,然后才左右监听
        viewPager2.registerOnPageChangeCallback(object :ViewPager2.OnPageChangeCallback(){
            override fun onPageSelected(position: Int) {
                super.onPageSelected(position)
                photoTag.text="${position+1}/${phootList?.size}"
            }
        })
  1. 设定值,每次赋值,第三个参数是动画效果 即可.改主题Dark,background可直接选取,改为白字
  viewPager2.setCurrentItem(arguments?.getInt("PHOTO_POSITION") ?: 0, false)

【Android-Kotlin-Volley】图片画廊学习笔记_第28张图片

  1. 尝试新的layout布局,据图片大小自适应布局,并且把centcrop给去掉
    【Android-Kotlin-Volley】图片画廊学习笔记_第29张图片
    【Android-Kotlin-Volley】图片画廊学习笔记_第30张图片
  2. 预览,设置卡片间隔4dp,两卡片间会有8dp,卡片间设为2dp,外包裹设为2dp
    【Android-Kotlin-Volley】图片画廊学习笔记_第31张图片
    【Android-Kotlin-Volley】图片画廊学习笔记_第32张图片
  3. 绘制纯色矢量图
    1)建立drawable的xml文件
    【Android-Kotlin-Volley】图片画廊学习笔记_第33张图片
    2)画一个方块/和当时画登录注册一样,然后glide替换一下
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <shape>
            <solid android:color="#EFEFF4"/>
            <size android:height="30dp"
                  android:width="40dp"/>
        </shape>
    </item>
</selector>

4.4 新特性,调整瀑布布局,加入点赞和收藏

  1. 观察API,在图片缓存前,直接确定高度
    在这里插入图片描述
  2. BindViewHolder中,通过设置layoutParams的高度
 holder.itemView.imageView.layoutParams.height=getItem(position).photoHeight
  1. 虽然成功了,但图片间隙乱七八糟的,emmmm,之前调整的centcrop在限定高度后可以使用了,使照片和容器一起收缩变化,搞定。
  2. 利用API的其他信息,若用户ID,喜爱和收藏的数量。
@Parcelize data class PhotoItem(
    @SerializedName("webformatURL")val previewUrl:String,
    @SerializedName("id")val photoId:Int,
    @SerializedName("largeImageURL")val fullUrl:String,
    @SerializedName("webformatHeight")val photoHeight:Int,
    @SerializedName("user") val photoUser:String,
    @SerializedName("likes") val photoLikes:Int,
    @SerializedName("favorites") val photoFavorites:Int
):Parcelable
  1. 修改条目的布局,连接三个边
    【Android-Kotlin-Volley】图片画廊学习笔记_第34张图片
  2. 增设两个矢量图,用于反馈点赞和收藏
    【Android-Kotlin-Volley】图片画廊学习笔记_第35张图片
  3. 增添控件
    【Android-Kotlin-Volley】图片画廊学习笔记_第36张图片
  4. 使用with语句()内的对象写出来,后面的参数中可以不写而默认调用。.with
    【Android-Kotlin-Volley】图片画廊学习笔记_第37张图片

4.5准备下载图片

  1. 保存图片,先建立一个矢量图。和FarawayPlayer一样建立两个颜色的,然后编辑xml按下有变化(通过Selector选择器->普通状态和按下状态)。
    【Android-Kotlin-Volley】图片画廊学习笔记_第38张图片
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_pressed="true" android:drawable="@drawable/ic_file_download_gray" />
    <item android:drawable="@drawable/ic_file_download_white" />
</selector>
  1. 大图的界面是FrameLayout改为->ConstraintLayout便于布局,加入DownLoad上下拖动图片对齐。
    【Android-Kotlin-Volley】图片画廊学习笔记_第39张图片
  2. 保存到系统相册 公共空间,添加清单权限,比INTERNET严格(危险权限还需动态声明)29放松了,写入不需要,但读取需要。最好动态注册
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
  1. 图片保存方式PagerPhotoFragment
    1)将原图下载:网址->下载->作为图片存储
  1. 从显示的图片转换为位图存储(对应的ViewHolder->找到图片)
    private fun savePhoto() {
        val holder=(viewPager2[0] as RecyclerView).findViewHolderForAdapterPosition(viewPager2.currentItem)//获取第一个元素 转为RecycleView
            as PagerPhotoListAdapter.PagerPhotoViewHolder
        val bitmap=holder.itemView.pagerPhoto.drawable.toBitmap()
        //API29前使用媒体资源索引管理器
        if (MediaStore.Images.Media.insertImage(requireContext().contentResolver,bitmap,null,null)==null){

            Toast.makeText(requireContext(), "存储失败", Toast.LENGTH_SHORT).show()

        }else{
            Toast.makeText(requireContext(),"存储成功",Toast.LENGTH_SHORT).show()

        }

    }
  1. 但这种方法已经别弃用了,若target版本改为>29就会报错,修改分两步
    在这里插入图片描述
        val saveUri=requireContext().contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
        ContentValues())?:kotlin.run {
            Toast.makeText(requireContext(), "存储失败", Toast.LENGTH_SHORT).show()
            return
        }//里面可以空也可以具体化
        //2.真正写入  use用好后会自动关闭流  90压缩率
        requireContext().contentResolver.openOutputStream(saveUri).use {
            if (bitmap.compress(Bitmap.CompressFormat.JPEG,90,it)==true){
                Toast.makeText(requireContext(),"存储成功",Toast.LENGTH_SHORT).show()
            }else{
                Toast.makeText(requireContext(), "存储失败", Toast.LENGTH_SHORT).show()
            }
        }

class PagerPhotoFragment : Fragment() {



    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_pager_photo, container, false)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        val phootList: ArrayList<PhotoItem>? =arguments?.getParcelableArrayList<PhotoItem>("PHOTO_LIST")
        PagerPhotoListAdapter().apply {
            viewPager2.adapter=this//二代viewpager可接受listadapter
            //再给个数据
            submitList(phootList)
        }

        viewPager2.registerOnPageChangeCallback(object :ViewPager2.OnPageChangeCallback(){
            override fun onPageSelected(position: Int) {
                super.onPageSelected(position)
                photoTag.text="${position+1}/${phootList?.size}"
            }
        })
        viewPager2.setCurrentItem(arguments?.getInt("PHOTO_POSITION") ?: 0, false)
        //保存到系统相册 公共空间
        saveButton.setOnClickListener {

            //toast()
            if (Build.VERSION.SDK_INT < 29 && ContextCompat.checkSelfPermission(
                    requireContext(),
                    Manifest.permission.WRITE_EXTERNAL_STORAGE
                ) != PackageManager.PERMISSION_GRANTED
            ) {
                requestPermissions(//只有一个参数也要用数组arrayof
                    arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
                    1
                )
            }else{
                savePhoto()
            }
        }
    }

    override fun onRequestPermissionsResult(
            requestCode: Int,
            permissions: Array<out String>,
            grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        when (requestCode) {
            1 -> {
                if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                        savePhoto()
                    } else{
                    Toast.makeText(requireContext(), "存储失败", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }


    private fun savePhoto() {
        val holder=(viewPager2[0] as RecyclerView).findViewHolderForAdapterPosition(viewPager2.currentItem)//获取第一个元素 转为RecycleView
            as PagerPhotoListAdapter.PagerPhotoViewHolder
        val bitmap=holder.itemView.pagerPhoto.drawable.toBitmap()
/*        //API29前使用媒体资源索引管理器
        if (MediaStore.Images.Media.insertImage(requireContext().contentResolver,bitmap,null,null)==null){

            Toast.makeText(requireContext(), "存储失败", Toast.LENGTH_SHORT).show()

        }else{
            Toast.makeText(requireContext(),"存储成功",Toast.LENGTH_SHORT).show()

        }*/
        //29以后 手动制作
        //1. 定义路径 占位
        val saveUri=requireContext().contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
        ContentValues())?:kotlin.run {
            Toast.makeText(requireContext(), "存储失败", Toast.LENGTH_SHORT).show()
            return
        }//里面可以空也可以具体化
        //2.真正写入  use用好后会自动关闭流  90压缩率
        requireContext().contentResolver.openOutputStream(saveUri).use {
            if (bitmap.compress(Bitmap.CompressFormat.JPEG,90,it)==true){
                Toast.makeText(requireContext(),"存储成功",Toast.LENGTH_SHORT).show()
            }else{
                Toast.makeText(requireContext(), "存储失败", Toast.LENGTH_SHORT).show()
            }
        }
    }


}
  1. 目前是放在主线程运行,当图片大时最好放入其他线程,先标识关键字,标识可以挂起。就不能直接调用方法了,指定范围协程概念
    【Android-Kotlin-Volley】图片画廊学习笔记_第40张图片
    1)指定范围
    【Android-Kotlin-Volley】图片画廊学习笔记_第41张图片
    2)导包并使用协程调用,若无协程,点击保存可能会阻塞主线程,保存期间划不动界面
  implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-rc03'

在这里插入图片描述
在这里插入图片描述
3)但此时Toast就无法运行了//他是主线程的UI操作,使用MainScope().launch即可

    MainScope().launch {  Toast.makeText(requireContext(),"存储成功",Toast.LENGTH_SHORT).show()}

你可能感兴趣的:(项目,android)