版权声明:本文为博主原创文章,未经博主允许不得转载。
本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。
一、效果
多张图片,通过滑动,体现一个选中图片的效果。
二、Drawnable
Drawnable 是一个可画的对象,其可能是一张位图(BitmapDrawnable),也可能是一个图形(ShapeDrawnable),还有可能是一个图层(LayerDrawnable)。Drawnable 类似一个“内存画布”,我们可以在这上面画对象。与 View 相比, Drawnable 不需要去处理一些交互事件,只是把图片展示出来,不具备接收事件与用户交互的能力。
常见的 Drawnable 有:ShapeDrawnable、LayerDrawnable、StateListDrawnable、ClipDrawnable、AnimationDrawnable 等。
我们这边的 RevealView 使用自定义 Drawnable 来实现。
三、分析
1.水平滚动使用 ScrollView 来实现,在 ScrollView 中加载一层布局,布局中加载一组 ImageView。
2.一张图片有四个状态:(1)全灰色 (2)左边灰色,右边彩色 (3)全彩色 (4)左边彩色,右边灰色。当处于(2)、(4)状态时。我们采用灰色的图片与对应的彩色图片进行剪裁拼接。
四、实现
1.Drawable 的 Bounds
在 Drawable 里面有一个 Bounds,通过 setBounds(int left, int top, int right, int bottom) 进行设置,Drawable 将被绘制在 setBounds 四个参数组成的矩形区域内。
我们可以在其他地方通过 getBounds 获取到 Drawable 的绘制范围。
2.矩形区域的剪裁
这边介绍个 Gravity 的 apply 方法。
/**
* Apply a gravity constant to an object. This supposes that the layout direction is LTR.
*
* @param gravity The desired placement of the object, as defined by the
* constants in this class.
* @param w The horizontal size of the object.
* @param h The vertical size of the object.
* @param container The frame of the containing space, in which the object
* will be placed. Should be large enough to contain the
* width and height of the object.
* @param outRect Receives the computed frame of the object in its
* container.
*/
public static void apply(int gravity, int w, int h, Rect container, Rect outRect) {
apply(gravity, w, h, container, 0, 0, outRect);
}
apply(int gravity, int w, int h, Rect container, Rect outRect)
gravity : 表示截取开始的方向
w : 截取的宽度
h : 截取的高度
containter : 被截取的矩形
outRect : 截取的区域(在 Canvas 上 draw 的时候,只会画出这块区域的内容)
3.单个图片的绘制
我们先来实现单个图片的拼接功能。效果如下:
public class RevealDrawable extends Drawable {
//灰色图
private Drawable mUnselectedDrawable;
//彩色图
private Drawable mSelectedDrawable;
public RevealDrawable(Drawable unselected, Drawable selected) {
mUnselectedDrawable = unselected;
mSelectedDrawable = selected;
}
@Override
public void draw(@NonNull Canvas canvas) {
Rect bounds = getBounds();
int height = bounds.height();
int width = bounds.width();
Rect r = new Rect();
Gravity.apply(
Gravity.LEFT, //从左边还是右边开始抠
width/2, //目标矩形的宽
height, //目标矩形的高
bounds, //被抠出来的rect
r); //目标rect
//不进行 save 和 restore 的话,第二次进行 clip 裁剪会在第一次裁剪基础上进行裁剪
canvas.save();
canvas.clipRect(r);
mUnselectedDrawable.draw(canvas);
canvas.restore();
Gravity.apply(
Gravity.RIGHT, //从左边还是右边开始抠
width/2, //目标矩形的宽
height, //目标矩形的高
bounds, //被抠出来的rect
r); //目标rect
canvas.save();
canvas.clipRect(r);
mSelectedDrawable.draw(canvas);
canvas.restore();
}
@Override
protected void onBoundsChange(Rect bounds) {
//定好两个 Drawable 图片的宽高为 RevealDrawable 的边界 bounds
mUnselectedDrawable.setBounds(bounds);
mSelectedDrawable.setBounds(bounds);
}
//getIntrinsicWidth() 和 getIntrinsicHeight(),用来取得 Drawable 的固有的宽度和高度。
@Override
public int getIntrinsicWidth() {
return Math.max(mSelectedDrawable.getIntrinsicWidth(),
mUnselectedDrawable.getIntrinsicWidth());
}
@Override
public int getIntrinsicHeight() {
return Math.max(mSelectedDrawable.getIntrinsicHeight(),
mUnselectedDrawable.getIntrinsicHeight());
}
@Override
public void setAlpha(@IntRange(from = 0, to = 255) int i) {
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
}
@Override
public int getOpacity() {
return PixelFormat.UNKNOWN;
}
}
在 draw 方法中,分别对灰色和彩色图片进行相应区域的剪裁和绘制,记得在第一次剪裁绘制前进行 save,绘制完成之后使用 resore,这样进行第二次剪裁时候是在最初始的 canvas 上进行,否则是在第一次剪裁出来的区域上进行再次剪裁。
重写 getIntrinsicWidth 和 getIntrinsicHeight 是为了在 View 使用 wrap_content 的时候,返回一个大小,Drawable 类中默认是返回-1,这会导致不能显示。
在 onBoundsChange 中设置两个 Drawable 的 bounds 与 RevealDrawable 的 bounds 一致,即把这两个 Drawable 绘制到 RevealDrawable 所要绘制的区域。
MainActivity 的代码:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
RevealDrawable revealDrawable = new RevealDrawable(getResources().getDrawable(R.drawable.avft),
getResources().getDrawable(R.drawable.avft_active));
setContentView(R.layout.activity_main);
ImageView imageView = (ImageView)findViewById(R.id.imageView);
imageView.setImageDrawable(revealDrawable);
}
}
avft 和 avft_active 为对应图片的灰色和彩色。
布局文件 activity_main.xml :
<android.support.constraint.ConstraintLayout 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"
tools:context="xiaoyue.com.revealview.MainActivity">
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
就直接放一个 ImageView。
4.Drawable 的 mLevel
Drawable 自带有一个 int 类型的 mLevel 属性,这个属性值的大小在 0 - 10000 之间。同时,Drawable 为 mLevel 提供了set 和 get 方法,另外还提供了一个对 mLevel 值改变的监听方法 onLevelChange。
private int mLevel = 0;
/**
* Specify the level for the drawable. This allows a drawable to vary its
* imagery based on a continuous controller, for example to show progress
* or volume level.
*
* <p>If the new level you are supplying causes the appearance of the
* Drawable to change, then it is responsible for calling
* {@link #invalidateSelf} in order to have itself redrawn, <em>and</em>
* true will be returned from this function.
*
* @param level The new level, from 0 (minimum) to 10000 (maximum).
*
* @return Returns true if this change in level has caused the appearance
* of the Drawable to change (hence requiring an invalidate), otherwise
* returns false.
*/
public final boolean setLevel(@IntRange(from=0,to=10000) int level) {
if (mLevel != level) {
mLevel = level;
return onLevelChange(level);
}
return false;
}
/**
* Retrieve the current level.
*
* @return int Current level, from 0 (minimum) to 10000 (maximum).
*/
public final @IntRange(from=0,to=10000) int getLevel() {
return mLevel;
}
/** Override this in your subclass to change appearance if you vary based
* on level.
* @return Returns true if the level change has caused the appearance of
* the Drawable to change (that is, it needs to be drawn), else false
* if it looks the same and there is no need to redraw it since its
* last level.
*/
protected boolean onLevelChange(int level) {
return false;
}
在 Drawable 源码中,setLevel 方法中对 mLevel 值改变的时候,调用 onLevelChange(int level) 方法,从而达到对 mLevel 值变化的监听,我们只需重写对应的 onLevelChange 方法即可。
Drawable 里面还有个 int 数组 mStateSet,效果与 mLevel 类似。
在这里我们采用 mLevel 的 0 - 10000 来表示一张图片左下角对应的位置,从而标识变化过程所有状态。
(1)全灰色 —- 0 或10000
(2)左边灰色,右边彩色 —- 0 - 5000
(3)全彩色 —- 5000
(4)左边彩色,右边灰色 5000 - 10000
5.通过 mLevel 绘制对应的图片状态
我们先来实现通过点击来改变 Drawable 的 level ,从而改变图片的显示。
public class RevealDrawable extends Drawable {
//灰色图
private Drawable mUnselectedDrawable;
//彩色图
private Drawable mSelectedDrawable;
public RevealDrawable(Drawable unselected, Drawable selected) {
mUnselectedDrawable = unselected;
mSelectedDrawable = selected;
}
@Override
public void draw(@NonNull Canvas canvas) {
//获取当前 Level
int level = getLevel();
if (level == 0 || level == 10000) {
//灰色图
mUnselectedDrawable.draw(canvas);
} else if (level == 5000) {
//彩色图
mSelectedDrawable.draw(canvas);
} else {
//一半彩色一半灰色
int uSelectGravity, selectGravity;
float uSelectRadio, selectRadio;
if (level < 5000) {
uSelectGravity = Gravity.LEFT;
selectGravity = Gravity.RIGHT;
uSelectRadio = 1 - level/5000f;
} else {
uSelectGravity = Gravity.RIGHT;
selectGravity = Gravity.LEFT;
uSelectRadio = level/5000f - 1;
}
selectRadio = 1 - uSelectRadio;
Rect bounds = getBounds();
int height = bounds.height();
int width = bounds.width();
//画灰色部分
Rect r = new Rect();
Gravity.apply(
uSelectGravity, //从左边还是右边开始抠
(int)(width * uSelectRadio),//目标矩形的宽
height, //目标矩形的高
bounds, //被抠出来的rect
r); //目标rect
//不进行 save 和 restore 的话,第二次进行 clip 裁剪会在第一次裁剪基础上进行裁剪
canvas.save();
canvas.clipRect(r);
mUnselectedDrawable.draw(canvas);
canvas.restore();
//画彩色部分
Gravity.apply(
selectGravity, //从左边还是右边开始抠
(int)(width * selectRadio), //目标矩形的宽
height, //目标矩形的高
bounds, //被抠出来的rect
r); //目标rect
canvas.save();
canvas.clipRect(r);
mSelectedDrawable.draw(canvas);
canvas.restore();
}
}
@Override
protected void onBoundsChange(Rect bounds) {
//定好两个 Drawable 图片的宽高为 RevealDrawable 的边界 bounds
mUnselectedDrawable.setBounds(bounds);
mSelectedDrawable.setBounds(bounds);
}
//getIntrinsicWidth() 和 getIntrinsicHeight(),用来取得 Drawable 的固有的宽度和高度。
@Override
public int getIntrinsicWidth() {
return Math.max(mSelectedDrawable.getIntrinsicWidth(),
mUnselectedDrawable.getIntrinsicWidth());
}
@Override
public int getIntrinsicHeight() {
return Math.max(mSelectedDrawable.getIntrinsicHeight(),
mUnselectedDrawable.getIntrinsicHeight());
}
@Override
protected boolean onLevelChange(int level) {
//重新进行绘制
invalidateSelf();
return true;
}
@Override
public void setAlpha(@IntRange(from = 0, to = 255) int i) {
}
@Override
public void setColorFilter(@Nullable ColorFilter colorFilter) {
}
@Override
public int getOpacity() {
return PixelFormat.UNKNOWN;
}
}
绘制时候先判断 level,从而确定图片状态,在半彩色半灰色的情况下,分别计算出灰色和彩色在左边还是右边,以及各自绘制区域的宽度。最后,在 level 值的监听方法 onLevelChange 中进行重绘。
MainActivity 的代码:
public class MainActivity extends AppCompatActivity {
private int level = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
RevealDrawable revealDrawable = new RevealDrawable(getResources().getDrawable(R.drawable.avft),
getResources().getDrawable(R.drawable.avft_active));
setContentView(R.layout.activity_main);
final ImageView imageView = (ImageView)findViewById(R.id.view);
imageView.setImageDrawable(revealDrawable);
//设置初始 level 为 0
imageView.setImageLevel(level);
imageView.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
if (level == 10000) {
level = 0;
}
level += 1000;
imageView.setImageLevel(level);
}
});
}
}
为图片添加一个点击事件,每次点击改变图片 level,从而进行重绘。布局文件不变。
6.添加一个自定义的 HorizontalScrollView
这里实现最终的效果。
public class GrallaryHorizonalScrollView extends HorizontalScrollView{
private LinearLayout layout;
private int iconWidth;
public GrallaryHorizonalScrollView(Context context) {
super(context);
init();
}
public GrallaryHorizonalScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
layout = new LinearLayout(getContext());
layout.setLayoutParams(params);
//把 LinearLayout 添加到 GrallaryHorizonalScrollView
addView(layout);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
//保存一张图片的宽度
View v = layout.getChildAt(0);
iconWidth = v.getWidth();
//计算整个 LinearLayout 的左右边距,否则的话第一个图片会在最左边显示,无法显示在中间
int llPad = getWidth()/2 - iconWidth/2;
layout.setPadding(llPad, 0, llPad, 0);
}
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
//遍历计算每张图片的 level
for (int i=0; i<layout.getChildCount(); i++) {
//l/(float)iconWidth + 1) 计算出距最初位置向左移动了几张图片的距离
//(i - l/(float)iconWidth + 1) * 5000 以第一张图片初始位置为 5000,计算对应的现在各个图片的 level
float level = (i - l/(float)iconWidth + 1) * 5000;
//level < 0 则为 0, level > 10000 则为 10000
if (level < 0) {
level = 0;
} else if (level > 10000){
level = 10000;
}
((ImageView)layout.getChildAt(i)).setImageLevel((int)level);
}
}
public void addImageView(Drawable[] revealDrawables) {
//遍历图片数组,添加到 LinearLayout
for (int i=0; i<revealDrawables.length; i++) {
ImageView view = new ImageView(getContext());
view.setImageDrawable(revealDrawables[i]);
layout.addView(view);
if (i == 0) {
view.setImageLevel(5000);
}
}
}
}
在 GrallaryHorizonalScrollView 中添加一个 LinearLayout,在 addImageView 方法中,把RevealDrawable 图片数组添加到 LinearLayout 中。重写 onScrollChanged 方法,当进行滑动的时候,对各个 RevealDrawable 的 level 重新进行赋值。
自定义的Drawable RevealDrawable 不需要改变。MainActivity 的代码:
public class MainActivity extends AppCompatActivity {
private int[] mImgIds = new int[] { //7个
R.drawable.avft,
R.drawable.box_stack,
R.drawable.bubble_frame,
R.drawable.bubbles,
R.drawable.bullseye,
R.drawable.circle_filled,
R.drawable.circle_outline
};
private int[] mImgIds_active = new int[] {
R.drawable.avft_active, R.drawable.box_stack_active, R.drawable.bubble_frame_active,
R.drawable.bubbles_active, R.drawable.bullseye_active, R.drawable.circle_filled_active,
R.drawable.circle_outline_active
};
private int level = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Drawable[] revealDrawables = new Drawable[mImgIds.length];
for (int i=0; i<mImgIds.length; i++) {
RevealDrawable revealDrawable = new RevealDrawable(
getResources().getDrawable(mImgIds[i]),
getResources().getDrawable(mImgIds_active[i])
);
revealDrawables[i] = revealDrawable;
}
GrallaryHorizonalScrollView ghs = (GrallaryHorizonalScrollView)findViewById(R.id.ghs);
ghs.addImageView(revealDrawables);
}
}
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
tools:context="com.xiaoyue.revealview.MainActivity">
<com.xiaoyue.revealview.GrallaryHorizonalScrollView
android:id="@+id/ghs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:background="#AA444444"
android:scrollbars="none"
/>
</android.support.constraint.ConstraintLayout>