首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

Android 下拉拉伸圖片效果:Kotlin 與 Java 雙版本實現 QQ 空間風格 PullToZoomListView

     編輯:Java 綜合教學

Android 仿 QQ 空間下拉拉伸頭圖:PullToZoomListView 核心原理、Java/Kotlin 雙實現、Anko 擴展與 Kotlin Extensions 實戰

「下拉頭圖放大、釋放回彈」是社交類 App 標配交互。本文拆解 PullToZoomListView 核心邏輯——overScrollBy 攔截越界滑動、onScrollChanged 動態縮放、onTouchEvent.ACTION_UP 啟動回彈動畫,提供 **Java 與 Kotlin 完整雙版本代碼**,並演示 Kotlin Android Extensions 視圖綁定、Anko DSL 布局優勢。

一、核心原理圖解

ListView (含 HeaderView)
└── HeaderView (FrameLayout/LinearLayout)
    └── ImageView (ScaleType.CENTER_CROP)
        
交互流程:
1. 用戶下拉 → ListView 觸底 → overScrollBy(deltaY<0) 攔截
2. deltaY 為負 → ImageView 高度 = 原高 - deltaY(變大)
3. 同時 HeaderView layout(0,0) 保持頂部貼邊
4. 用戶抬手 → onTouchEvent(ACTION_UP) → ResetAnimation(300ms)
5. 動畫插值器將高度從當前值線性插值回原始高度
6. requestLayout() 觸發重繪,完成平滑回彈

二、Java 版 PullToZoomListView 完整代碼


public class PullToZoomListView extends ListView {
    private ImageView mImageView;
    private int mImageViewHeight = -1;
    private int mDrawableMaxHeight = -1;

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

    public void setPullToZoomImageView(ImageView imageView) {
        this.mImageView = imageView;
        mImageView.setScaleType(ScaleType.CENTER_CROP);
    }

    public void setViewBounds() {
        if (mImageViewHeight == -1) {
            mImageViewHeight = mImageView.getHeight();
            if (mImageViewHeight < 0) {
                mImageViewHeight = getContext().getResources()
                        .getDimensionPixelSize(R.dimen.size_default);
            }
        }
    }

    @Override
    protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY,
                                   int scrollRangeX, int scrollRangeY,
                                   int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
        boolean isCollapse = resizeOverScrollBy(deltaY);
        return isCollapse ? true : super.overScrollBy(deltaX, deltaY, scrollX, scrollY,
                scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        View header = (View) mImageView.getParent();
        if (header.getTop() < 0 && mImageView.getHeight() > mImageViewHeight) {
            mImageView.getLayoutParams().height = Math.max(
                    mImageView.getHeight() + header.getTop(), mImageViewHeight);
            header.layout(header.getLeft(), 0, header.getRight(), header.getHeight());
            mImageView.requestLayout();
        }
    }

    private boolean resizeOverScrollBy(int deltaY) {
        if (deltaY < 0) { // 下拉
            mImageView.getLayoutParams().height = mImageView.getHeight() - deltaY;
            mImageView.requestLayout();
        } else { // 上拉回彈
            if (mImageView.getHeight() > mImageViewHeight) {
                mImageView.getLayoutParams().height = Math.max(
                        mImageView.getHeight() - deltaY, mImageViewHeight);
                mImageView.requestLayout();
            }
        }
        return false;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_UP) {
            ResetAnimation anim = new ResetAnimation(mImageView, mImageViewHeight);
            anim.setDuration(300);
            mImageView.startAnimation(anim);
        }
        return super.onTouchEvent(ev);
    }

    private class ResetAnimation extends Animation {
        private final ImageView mView;
        private final int targetHeight;
        private final int originalHeight;
        private final int extraHeight;

        ResetAnimation(ImageView view, int targetHeight) {
            this.mView = view;
            this.targetHeight = targetHeight;
            this.originalHeight = view.getHeight();
            this.extraHeight = originalHeight - targetHeight;
        }

        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            mView.getLayoutParams().height = (int) (originalHeight - extraHeight * interpolatedTime);
            mView.requestLayout();
            super.applyTransformation(interpolatedTime, t);
        }
    }
}

三、Kotlin 版 PullToZoomListView:空安全、擴展函數、內部類優雅重寫


open class PullToZoomListView @JvmOverloads constructor(
    context: Context, attrs: AttributeSet
) : ListView(context, attrs) {

    private var mImageView: ImageView? = null
    private var mImageViewHeight = -1
    private val mDrawableMaxHeight = -1

    fun setPullToZoomImageView(imageView: ImageView) {
        mImageView = imageView
        mImageView!!.scaleType = ScaleType.CENTER_CROP
    }

    fun setViewBounds() {
        if (mImageViewHeight == -1) {
            mImageViewHeight = mImageView!!.height
            if (mImageViewHeight < 0) {
                mImageViewHeight = context.resources.getDimensionPixelSize(R.dimen.size_default)
            }
        }
    }

    override fun overScrollBy(deltaX: Int, deltaY: Int, scrollX: Int, scrollY: Int,
                              scrollRangeX: Int, scrollRangeY: Int,
                              maxOverScrollX: Int, maxOverScrollY: Int, isTouchEvent: Boolean): Boolean {
        val isCollapse = resizeOverScrollBy(deltaY)
        return if (isCollapse) true else super.overScrollBy(deltaX, deltaY, scrollX, scrollY,
                scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent)
    }

    override fun onScrollChanged(l: Int, t: Int, oldl: Int, oldt: Int) {
        super.onScrollChanged(l, t, oldl, oldt)
        val header = mImageView!!.parent as View
        if (header.top < 0 && mImageView!!.height > mImageViewHeight) {
            mImageView!!.layoutParams.height = Math.max(
                    mImageView!!.height + header.top, mImageViewHeight)
            header.layout(header.left, 0, header.right, header.height)
            mImageView!!.requestLayout()
        }
    }

    private fun resizeOverScrollBy(deltaY: Int): Boolean {
        if (deltaY < 0) {
            mImageView!!.layoutParams.height = mImageView!!.height - deltaY
            mImageView!!.requestLayout()
        } else if (mImageView!!.height > mImageViewHeight) {
            mImageView!!.layoutParams.height = Math.max(mImageView!!.height - deltaY, mImageViewHeight)
            mImageView!!.requestLayout()
        }
        return false
    }

    override fun onTouchEvent(ev: MotionEvent): Boolean {
        if (ev.action == MotionEvent.ACTION_UP) {
            val anim = ResetAnimation(mImageView!!, mImageViewHeight).apply { duration = 300 }
            mImageView!!.startAnimation(anim)
        }
        return super.onTouchEvent(ev)
    }

    inner class ResetAnimation(private val mView: ImageView, private val targetHeight: Int) : Animation() {
        private val originalHeight: Int = mView.height
        private val extraHeight: Int = originalHeight - targetHeight

        override fun applyTransformation(interpolatedTime: Float, t: Transformation) {
            mView.layoutParams.height = (originalHeight - extraHeight * interpolatedTime).toInt()
            mView.requestLayout()
            super.applyTransformation(interpolatedTime, t)
        }
    }
}

四、MainActivity 雙版本調用

Java


public class MainActivity extends AppCompatActivity {
    private PullToZoomListView lv;
    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        lv = findViewById(R.id.main_lv);
        View header = View.inflate(this, R.layout.layout_lv_header, null);
        lv.addHeaderView(header);
        ArrayAdapter adapter = new ArrayAdapter<>(this,
                android.R.layout.simple_list_item_1, 
                new String[]{"騰訊","阿裡","百度","新浪","C","Java","PHP","FB","Twitter","XML","HTML"});
        lv.setAdapter(adapter);
        ImageView iv = header.findViewById(R.id.header_img);
        lv.setPullToZoomImageView(iv);
    }
    @Override public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus) lv.setViewBounds();
    }
}

Kotlin + Kotlin Android Extensions (合成屬性)


class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        supportActionBar?.hide()
        if (Build.VERSION.SDK_INT >= 21) supportActionBar?.elevation = 0f

        val header = View.inflate(this, R.layout.layout_lv_header, null)
        main_lv.addHeaderView(header) // 合成屬性直接用 ID
        main_lv.adapter = ArrayAdapter(this,
                android.R.layout.simple_list_item_1,
                arrayOf("騰訊","阿裡","百度","新浪","C","Java","PHP","FB","Twitter","XML","HTML"))
        main_lv.setPullToZoomImageView(header_img) // 合成屬性
    }

    override fun onWindowFocusChanged(hasFocus: Boolean) {
        super.onWindowFocusChanged(hasFocus)
        if (hasFocus) main_lv.setViewBounds()
    }
}

五、Kotlin Android Extensions 快速上手

build.gradle (Module) 配置


apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    sourceSets {
        main.java.srcDirs += 'src/main/kotlin'
    }
}

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}

buildscript {
    ext.kotlin_version = '1.8.20'
    repositories { mavenCentral() }
    dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" }
}

三大核心功能

  1. .java → .kt 轉換:Code → Convert Java File to Kotlin File (Ctrl+Alt+Shift+K)
  2. Java 模塊 → Kotlin 模塊:Tools → Kotlin → Configure Kotlin in Project
  3. 合成屬性綁定 View:import kotlinx.android.synthetic.main.activity_main.* 後直接用 main_lv 代替 findViewById(R.id.main_lv)

六、Anko DSL 布局(可選進階)


// 代碼替代 XML,類型安全、可預覽
verticalLayout {
    listView(R.id.main_lv) {
        layoutParams = matchParent()
    }
}

七、總結

  • 核心機制:overScrollBy 攔截越界 → 動態修改 ImageView 高度 → onTouchEvent.UP 啟動回彈動畫
  • Kotlin 版利用空安全、擴展函數、apply/also、內部類、屬性委托,代碼更簡潔、空指釥安全
  • Kotlin Android Extensions 合成屬性**極大減少 findViewToId 樣板代碼**,但注意 Google 已宣佈棄用,新項目建議遷移 View Binding 或 Data Binding
  • 生產環境建議:將拉伸邏輯封裝為 PullToZoomScrollView 支援 RecyclerView/WebView 等任意可滾動 View
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved