(十一)Canvas 实例 - RevealView

版权声明:本文为博主原创文章,未经博主允许不得转载。

本文纯个人学习笔记,由于水平有限,难免有所出错,有发现的可以交流一下。

一、效果

多张图片,通过滑动,体现一个选中图片的效果。
这里写图片描述

二、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>

五、附

代码链接:http://download.csdn.net/detail/qq_18983205/9907871

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值