Android图片选择器-2
再来看一下图片选择器相关的UI开发,顺便梳理一下UI相关的知识点。常用的一些基础东西就不写了,这里只记录一些容易弄混,出错,不容易记住的东西
假设我们想要做成的图片选择器的UI类似于下图这样:

主题
我们希望UI上的元素尽可能的可配置,因此将相关的UI属性抽出来并定义成主题。比如:标题的颜色,文字的颜色,字体等。在开发的时候,还是像知乎那样提供两套基础的主题。一种主题的样式显示效果如上图所示,采用白底的头部。另一种,我们采用蓝底的头部。先回顾一下Style 和 Theme 相关的基础。新增Style和Theme的方式是一样的,都是直接在xml文件的<resources>结点中添加<style>结点,定义名称,添加属性。那么Style和Theme的区别是什么, Google官方是这么说的:
What's the difference between a style and a theme?
- A style applies to a View. In XML, you apply a style using the style attribute.
- A theme applies to an Activity or an entire app, rather than to an individual View. In XML, you apply a theme using the android:theme attribute.
Any style can be used as a theme. For example, you could apply the CodeFont style as a theme for an Activity, and all the text inside the Activity would use gray monospace font.
概括来讲,最大的区别是在于Theme是可以用在Activity或整个app。其实有些属性是只能定义在Theme中的,比如windowActionBar属性,就只能作为Theme的属性
当我们在android studio中新建一个module时,工具会自动帮我们生成一个style, 代码如下:
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
</resources>
默认会创建一个主题,即Apptheme,它继承自Theme.AppCompat.Light.DarkActionBar, 然后自定义了三个属性, colorPrimary, colorPrimaryDark, colorAccent。这三个属性是用来定义应用的基础色调。
colorPrimary: 应用的主色调,actionBar, Toolbar的底色colorPrimaryDark:应用的主要暗色调,statusBar默认使用的颜色colorAccent: 控件选中效果的默认颜色
android系统提供的属性,可以从这里查看。接下来定义自己的Theme。
第一步 我们在attr.xml中定义相关的属性
相关的属性后面可以结合着代码看
<resources>
<attr name="capture.textColor" format="reference|color"/>
<attr name="item.placeholder" format="reference|color"/>
<attr name="title.background" format="reference|color"/>
<attr name="title.commonText.color" format="reference|color"/>
<attr name="title.actionText.color" format="reference|color"/>
<attr name="title.arrowDown" format="reference"/>
<attr name="title.arrowUp" format="reference"/>
<attr name="title.statusBar.color" format="color"/>
<attr name="title.statusBar.darkFont" format="boolean"/>
<attr name="previewTitle.background" format="reference|color"/>
<attr name="previewTitle.commonText.color" format="reference|color"/>
<attr name="previewTitle.actionText.color" format="reference|color"/>
</resources>
第二步 在styles.xml中自定义Theme
<style name="CommonTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="item.placeholder">@color/item_placeholder</item>
<item name="capture.textColor">@color/capture_text_color</item>
<item name="title.background">@color/white</item>
<item name="title.commonText.color">@color/common_text</item>
<item name="title.actionText.color">@color/blue_1</item>
<item name="title.arrowDown">@drawable/arrow_down_black</item>
<item name="title.arrowUp">@drawable/arrow_up_black</item>
<item name="title.statusBar.color">@color/white</item>
<item name="title.statusBar.darkFont">false</item>
<item name="previewTitle.background">@android:color/black</item>
<item name="previewTitle.commonText.color">@android:color/white</item>
<item name="previewTitle.actionText.color">@color/blue_1</item>
</style>
<style name="QQTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="item.placeholder">@color/item_placeholder</item>
<item name="capture.textColor">@color/white</item>
<item name="title.background">@color/blue_1</item>
<item name="title.commonText.color">@color/white</item>
<item name="title.actionText.color">@color/white</item>
<item name="title.arrowDown">@drawable/arrow_down_white</item>
<item name="title.arrowUp">@drawable/arrow_up_white</item>
<item name="title.statusBar.color">@color/blue_1</item>
<item name="title.statusBar.darkFont">true</item>
<item name="previewTitle.background">@android:color/black</item>
<item name="previewTitle.commonText.color">@android:color/white</item>
<item name="previewTitle.actionText.color">@color/blue_1</item>
</style>
第三步 在布局文件中使用新定义的属性:
<TextView
android:id="@+id/selected_album"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:singleLine="true"
android:textColor="?title.commonText.color"
android:text="全部"
android:drawableRight="?title.arrowDown"
android:drawablePadding="6dp"
android:textSize="@dimen/title"
/>
第四步 为Activity或者app指定主题:
<activity android:name=".ui.ImagePreviewActivity" android:theme="@style/CommonTheme"></activity>
沉浸式模式
我们希望用户在使用图片选择器时,进行预览操作, 查看大图时,页面能够隐藏掉状态栏和标题,扩大可视区域。因此需要用到沉浸式模式。
沉浸式模式,也通常有人说是沉浸式导航,immersive 等,但描述的都是同一个东西。沉浸式表现的效果通常有以下种:
- 全屏显示,隐藏掉状态栏, 屏幕下方的导航栏,内容占据整个屏幕。
- 状态栏透明,内容直接到屏幕顶端,状态栏会遮盖在内容上方
- 状态栏透明,底部的导航栏也隐藏。
来看个Demo, 创建一个Activity, 在其布局文件中
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@mipmap/bg"
android:scaleType="centerCrop"/>
</LinearLayout>
隐藏掉状态栏
隐藏状态栏,我们只需要加对DecorView调用setSystemUiVisibility 方法,设置flag为View.SYSTEM_UI_FLAG_FULLSCREEN。同时隐藏掉actionBar。代码如下:
View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN);
ActionBar actionBar = getSupportActionBar();
actionBar.hide();
状态栏透明
让状态栏透明,同时让内容延伸到状态栏的下方。代码如下:
if (Build.VERSION.SDK_INT >= 21) {
View decorView = getWindow().getDecorView();
int flag = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
decorView.setSystemUiVisibility(flag);
getWindow().setStatusBarColor(Color.TRANSPARENT);
}
ActionBar actionBar = getSupportActionBar();
actionBar.hide();
在Android 5.0以后,系统提供了setStatusBarColor方法来手动设置statusBar的背景色,此处我们也仅考虑5.0以上的兼容。需要注意的是此处的flag需要用到SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN和SYSTEM_UI_FLAG_LAYOUT_STABLE这两个。这样才能保证状态栏显示,同时内容也能延伸到状态栏的下方。
隐藏底部的导航栏
隐藏底部的导航栏操作一般用得少,通常在视频,游戏类app中要多一些。而且隐藏导航栏的操作通常会有些问题,比如,我们用View.SYSTEM_UI_FLAG_HIDE_NAVIGATION和View.SYSTEM_UI_FLAG_FULLSCREEN两个flag, 结果发现,状态栏和导航栏都隐藏了,但是点击一下屏幕又都显示了。只能做一个透明的导航栏,代码如下:
if (Build.VERSION.SDK_INT >= 21) {
View decorView = getWindow().getDecorView();
int flag = View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|View.SYSTEM_UI_FLAG_HIDE_NAVIGATION ;
decorView.setSystemUiVisibility(flag);
getWindow().setNavigationBarColor(Color.TRANSPARENT);
getWindow().setStatusBarColor(Color.TRANSPARENT);
}
ActionBar actionBar = getSupportActionBar();
actionBar.hide();
视频app中全屏的显示
需要重写onWindowFocusChanged方法
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus && Build.VERSION.SDK_INT >= 19) {
View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
}
}
以上只是沉浸式最常用的操作,在国内android生态碎片化严重的情况下,会出现兼容性的问题,这篇文章中有部份记载,可以参考。
在项目中我引入了ImmersionBar来管理statusBar,compile com.gyf.barlibrary:barlibrary:2.3.0, 如果想详细了解相关的操作可以去看源码。
列表的显示
在上方的图中,我们可以看到,有两个列表,一个是相册列表,另一个是图片列表。同时,我们也知道,对于机册的图片的查询,都可以通过利用CursorLoader来从Content Provider中查,查询到的数据都以Cursor的形式返回,考虑到这点共性,我们可以将相册和图片列表要使用到的Adapter进行抽象,抽出其共性。我们定义一个父Adapter, 考虑到平时很少将ListView和 Cursor一起用,回顾一下:
对于ListView, 使用adapter可以屏蔽视图,数据的差异性,让ListView只做好自己份内的事就可以。对于ListView, android 提供了多种类别的Adapter供我们使用,如ArrayAdapter, CursorAdapter, SimpleAdapter等。这三个Adapter都是直接继承自BaseAdapter,其中的CursorAdapter是利用游标来返回数据,在需要从数据库中读取数据或者从Content Provider中读取数据时,使用会比较方便。使用CursorAdapter时,需要重写newView和bindView两个方法, 此处写个Demo回顾一下:
public class ImageCursorAdapter extends CursorAdapter {
private LayoutInflater cursorInflater;
public ImageCursorAdapter(Context context, Cursor c) {
super(context, c);
cursorInflater = (LayoutInflater) context.getSystemService(
Context.LAYOUT_INFLATER_SERVICE);
}
@Override
public int getCount() {
return super.getCount();
}
@Override
public Object getItem(int position) {
return super.getItem(position);
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
return cursorInflater.inflate(R.layout.image_list_item, parent, false);
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
ImageView imageView = (ImageView)view.findViewById(R.id.iv_image);
String imageUrl = cursor.getString( cursor.getColumnIndex( MediaStore.MediaColumns.DATA) );
Glide.with(context).load(Uri.fromFile(new File(imageUrl))).asBitmap()
.centerCrop().into(imageView);
}
}
如果我们使用RecyclerView,都是直接使用RecyclerView中的Adapter类,就不像ListView那样,可以使用CursorAdapter,但我们可以将Cursor传递到RecyclerView.Adapter中去。