自定义控件之重写ScrollView实现图片下拉放大

前言

因为公司项目要实现一个效果,在ScrollView没有向下滚动时,下拉(未重写前下拉是没有任何效果的)放大顶部的图片,当时去网上找了,记得以前见过很多这样的控件的,现在却找半天也很难找到一个,好不容易找到了2个,发现效果都和需求上面的效果有偏差,最后没有办法只能是自己写了,花费了半天时间研究出来了,同时为了记录实现思路,所以就有了此文章

效果

效果图

实现思路

拦截ScrollView的触摸滑动事件(ACTION_MOVE),记录下当前事件y轴坐标,判断当前ScrollView的Y轴滚动进度(getScrollY)是否等于0,等于0就与上次事件记录的位置进行对比,如果为正数就放大(X轴是从左往右,Y轴是从上往下,所以下拉时本次事件的Y轴会大于上次事件的Y轴),每次事件都通过设置ImageView的高度来放大图片控件(本来想用属性动画的,但是因为每个事件放大的比例非常小,所以最后就没使用,直接通过修改属性来实现),同时记录从开始到现在事件位置一共偏移了多少,当偏移量大于最大值的,就停止放大并将偏移量设置为最大值,当偏移量小于0时,则将偏移量设置为0,同时不再继续拦截事件。注意被放大的图片需要设置scaleType为centerCrop,这样当图片高度发生变化时,图片内容才会跟着大,当然其他几种模式有些模式也能跟着放大,但是具体可以自己去测试,我就不去测试了,毕竟我已经达到我要的效果了

好了,废话少说,先贴代码,再对代码进行说明

代码

package wang.raye.library;

import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.TypedArray;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.ScrollView;


/**
 * 重写让ScrollView有滚动监听(23以前是没有滚动监听的)
 * 拦截touch事件,让其支持下拉放大图片
 * Created by Raye on 2016/6/11.
 */
public class ZoomScrollView extends ScrollView {

    private View zoomView;
    /** 记录上次事件的Y轴*/
    private float mLastMotionY;
    /** 记录一个滚动了多少距离,通过这个来设置缩放*/
    private int allScroll = -1;
    /** 控件原本的高度*/
    private int height = 0;
    /** 被放大的控件id*/
    private int zoomId;
    /** 最大放大多少像素*/
    private int maxZoom;
    /** 滚动监听*/
    private ScrollViewListener scrollViewListener = null;
    private Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            allScroll -= 25;
            if(allScroll < 0){
                allScroll = 0;
            }
            LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) zoomView.getLayoutParams();
            lp.height = (int) (height + allScroll/2);
            zoomView.setLayoutParams(lp);
            if(allScroll != 0){
                handler.sendEmptyMessageDelayed(1,10);
            }else{
                allScroll = -1;
            }
        }
    };
    public ZoomScrollView(Context context) {
        super(context);
    }

    public ZoomScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(attrs);
    }

    public ZoomScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(attrs);
    }

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    public ZoomScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(attrs);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        zoomView = findViewById(zoomId);
    }

    private void init(AttributeSet attrs){
        TypedArray t = getContext().obtainStyledAttributes(attrs, R.styleable.ObservableScrollView);
        zoomId = t.getResourceId(R.styleable.ObservableScrollView_zoomId,-1);
        maxZoom = t.getDimensionPixelOffset(R.styleable.ObservableScrollView_maxZoom,0);
    }

    public void setScrollViewListener(ScrollViewListener scrollViewListener) {
        this.scrollViewListener = scrollViewListener;
    }


    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        if(zoomView == null || maxZoom == 0){
            return super.dispatchTouchEvent(event);
        }

        final int action = event.getAction();

        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            if(allScroll != -1){
                handler.sendEmptyMessageDelayed(1,10);
            }
            return super.dispatchTouchEvent(event);
        }

        switch (action) {
            case MotionEvent.ACTION_MOVE: {
                final float y = event.getY();
                final float diff, absDiff;
                diff = y - mLastMotionY;
                mLastMotionY = y;
                absDiff = Math.abs(diff);
                if(allScroll >= 0 && absDiff > 1){
                    allScroll += diff;

                    if(allScroll < 0){
                        allScroll = 0;
                    }else if(allScroll > maxZoom){
                        allScroll = maxZoom;
                    }
                    LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) zoomView.getLayoutParams();
                    lp.height = (int) (height + allScroll/2);
                    zoomView.setLayoutParams(lp);
                    if(allScroll == 0){
                        allScroll = -1;
                    }
                    return false;
                }
                if (isReadyForPullStart()) {
                    if (absDiff > 0 ) {
                        if (diff >= 1f && isReadyForPullStart()) {
                            mLastMotionY = y;
                            allScroll = 0;
                            height = zoomView.getHeight();
                            return true;
                        }
                    }
                }
                break;
            }


        }

        return super.dispatchTouchEvent(event);
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if(allScroll != -1){
            Log.i("ScrollView","onTouchEvent");
            return false;
        }
        return super.onTouchEvent(ev);
    }



    /**
     * 返回是否可以开始放大
     * @return
     */
    protected boolean isReadyForPullStart() {
        return getScrollY() == 0;
    }


    @Override
    protected void onScrollChanged(int x, int y, int oldx, int oldy) {
        super.onScrollChanged(x, y, oldx, oldy);
        if (scrollViewListener != null) {
            scrollViewListener.onScrollChanged(this, x, y, oldx, oldy);
        }
    }
    public interface ScrollViewListener {

        void onScrollChanged(ZoomScrollView scrollView, int x, int y, int oldx, int oldy);

    }
}

重要点,从上往下

private Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            allScroll -= 25;
            if(allScroll < 0){
                allScroll = 0;
            }
            LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) zoomView.getLayoutParams();
            lp.height = (int) (height + allScroll/2);
            zoomView.setLayoutParams(lp);
            if(allScroll != 0){
                handler.sendEmptyMessageDelayed(1,10);
            }else{
                allScroll = -1;
            }
        }
    };

这里是当ACTION_UP事件发生时,如果图片还在放大状态,就模拟动画效果,吧图片缩放回去,当然是可以用属性动画的,只是我之前没用属性动画,所以这里也直接用这个了

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        zoomView = findViewById(zoomId);
    }

这里是当控件从xml中初始化完成的生命周期方法,在这里我们找到被放大的图片控件

private void init(AttributeSet attrs){
        TypedArray t = getContext().obtainStyledAttributes(attrs, R.styleable.ObservableScrollView);
        zoomId = t.getResourceId(R.styleable.ObservableScrollView_zoomId,-1);
        maxZoom = t.getDimensionPixelOffset(R.styleable.ObservableScrollView_maxZoom,0);
    }

这段代码相信很容易看懂,就是获取2个自定义属性,一个是被放大的图片控件id,一个是最大的放大像素

最主要的地方,事件拦截
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        if(zoomView == null || maxZoom == 0){
            return super.dispatchTouchEvent(event);
        }

        final int action = event.getAction();

        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            if(allScroll != -1){
                handler.sendEmptyMessageDelayed(1,10);
            }
            return super.dispatchTouchEvent(event);
        }

        switch (action) {
            case MotionEvent.ACTION_MOVE: {
                final float y = event.getY();
                final float diff, absDiff;
                diff = y - mLastMotionY;
                mLastMotionY = y;
                absDiff = Math.abs(diff);
                if(allScroll >= 0 && absDiff > 1){
                    allScroll += diff;

                    if(allScroll < 0){
                        allScroll = 0;
                    }else if(allScroll > maxZoom){
                        allScroll = maxZoom;
                    }
                    LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) zoomView.getLayoutParams();
                    lp.height = (int) (height + allScroll/2);
                    zoomView.setLayoutParams(lp);
                    if(allScroll == 0){
                        allScroll = -1;
                    }
                    return false;
                }
                if (isReadyForPullStart()) {
                    if (absDiff > 0 ) {
                        if (diff >= 1f && isReadyForPullStart()) {
                            mLastMotionY = y;
                            allScroll = 0;
                            height = zoomView.getHeight();
                            return true;
                        }
                    }
                }
                break;
            }


        }

        return super.dispatchTouchEvent(event);
    }
详细说明
if(zoomView == null || maxZoom == 0){
       return super.dispatchTouchEvent(event);
}

当控件为空和最大放大像素为0 的时候,不进行事件拦截

        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            if(allScroll != -1){
                handler.sendEmptyMessageDelayed(1,10);
            }
            return super.dispatchTouchEvent(event);
        }

当事件取消和手指松开时,判断当前偏移量(allScroll )是否回到了最初状态-1,如果没有说明图片没有缩放,要缩放回去

            case MotionEvent.ACTION_MOVE: {
                final float y = event.getY();
                final float diff, oppositeDiff, absDiff;
                diff = y - mLastMotionY;
                mLastMotionY = y;
                absDiff = Math.abs(diff);
                if( allScroll >= 0 && absDiff > 1){
                    allScroll += diff;

                    if(allScroll < 0){
                        allScroll = 0;
                    }else if(allScroll > maxZoom){
                        allScroll = maxZoom;
                    }
                    LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) zoomView.getLayoutParams();
                    lp.height = (int) (height + allScroll/2);
                    zoomView.setLayoutParams(lp);
                    if(allScroll == 0){
                        allScroll = -1;
                    }
                    return false;
                }
                if (isReadyForPullStart()) {
                    if (absDiff > 0 ) {
                        if (diff >= 1f && isReadyForPullStart()) {
                            mLastMotionY = y;
                            allScroll = 0;
                            height = zoomView.getHeight();
                            return true;
                        }
                    }
                }
                break;
            }

拦截移动事件,每次记录下Y轴坐标,当滚动为0的时候,就计算与上次坐标的偏移量,大于0就开始放大,每次放大总偏移值的二分之一,因为每次放大总偏移值的效果不大好看,同时判断总偏移值是否大于最大偏移值,大于就设置总偏移值为最大值,相当于停止放大。如果小于0,就把总偏移值设置为0,并且重置偏移值的为-1,-1的时候,就不会拦截事件

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if(allScroll != -1){
            Log.i("ScrollView","onTouchEvent");
            return false;
        }
        return super.onTouchEvent(ev);
    }

重写onTouchEvent,当偏移值不是-1的时候,说明图片在进行放大或缩放,这时候不能让ScrollView滚动,所以需要把onTouchEvent拦截掉

    protected boolean isReadyForPullStart() {
        return getScrollY() == 0;
    }

获取当前ScrollView的滚动位置,是0的时候才可以开始放大图片

最后说两句

控件中还有个监听,那个不用管,那个是为了获取滚动位置来设置标题栏透明度的,跟本文内容无关,所以就不详细说明了。当然这个自定义控件只是为了实现我项目中需求的效果,很简陋,实现方法也很简单,所以欢迎高手前来指点。需要效果图demo的请点击demo github地址,另外同时也欢迎大家吐槽交流(QQ群:123965382)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  • 一、Android开发初体验 二、Android与MVC设计模式模型对象存储着应用的数据和业务逻辑。模型类通常用来...
    为梦想战斗阅读 874评论 0 3
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,392评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • 1 洞庭湖畔,渔歌唱晚。正值初春,微风习习,夕阳下的湖水更显静谧悠然。 这时,一辆马车缓缓驶来,车中一个两鬓斑白的...
    凝流阅读 927评论 19 36
  • 认知革命与物种灭绝 人类在认知革命发生前,掌握了工具和火的使用,在极短的时间内走上了食物链的顶端,别的物种都没来得...
    大羊爱生活阅读 188评论 0 0