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

Android 自定義 View 進階:組合控件 ImageButton 實現與 ViewPager 指示器封裝

     編輯:Java 相關知識

自定義 View 分類回顧

Android 自定義 View 主要四類:

  1. 繼承 View:重寫 onDraw,配合 Paint/Canvas 繪製(如圖表、遊戲畫面)。
  2. 組合控件:繼承 ViewGroup (如 LinearLayout, FrameLayout),在構造函數中 inflate 佈局,封裝子 View 邏輯(如標題欄、複雜列表項)。
  3. 繼承具體 View:如 TextView、ImageView、Button,擴展特定行為。
  4. 自定義 ViewGroup:重寫 onMeasure/onLayout,實現特殊佈局(如流式佈局、標籤雲)。

本文重點演示組合控件模式:FrameLayout 封裝 Button+ImageView 實現 ImageButton,以及封裝 ViewPager+指示器的複雜組合控件。

案例一:自定義 ImageButton 組合控件

1. 佈局文件

<!-- res/layout/view_image_button.xml -->
<FrameLayout xmlns:android="https://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/btn_base"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="?attr/selectableItemBackground" />

    <ImageView
        android:id="@+id/img_icon"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="centerInside"
        android:clickable="false"
        android:focusable="false" />
</FrameLayout>

2. 自定義屬性

<!-- res/values/attrs.xml -->
<resources>
    <declare-styleable name="CustomImageButton">
        <attr name="iconSrc" format="reference" />
        <attr name="buttonText" format="string" />
        <attr name="textColor" format="color" />
    </declare-styleable>
</resources>

3. Java 實現


public class CustomImageButton extends FrameLayout {
    private ImageView ivIcon;
    private Button btnBase;

    public CustomImageButton(Context context) {
        this(context, null);
    }

    public CustomImageButton(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

    private void init(Context context, AttributeSet attrs) {
        // 1. 加載佈局
        LayoutInflater.from(context).inflate(R.layout.view_image_button, this, true);
        ivIcon = findViewById(R.id.img_icon);
        btnBase = findViewById(R.id.btn_base);

        // 2. 解析自定義屬性
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CustomImageButton);
        try {
            int iconRes = a.getResourceId(R.styleable.CustomImageButton_iconSrc, 0);
            if (iconRes != 0) ivIcon.setImageResource(iconRes);

            String text = a.getString(R.styleable.CustomImageButton_buttonText);
            if (text != null) btnBase.setText(text);

            int textColor = a.getColor(R.styleable.CustomImageButton_textColor, 0);
            if (textColor != 0) btnBase.setTextColor(textColor);
        } finally {
            a.recycle(); // 必須回收
        }
    }

    // 對外暴露設置方法
    public void setIcon(int resId) { ivIcon.setImageResource(resId); }
    public void setText(CharSequence text) { btnBase.setText(text); }
    public void setOnClickListener(OnClickListener l) { btnBase.setOnClickListener(l); }
}

4. XML 中使用

<com.example.widget.CustomImageButton
    xmlns:app="https://schemas.android.com/apk/res-auto"
    android:layout_width="100dp"
    android:layout_height="100dp"
    app:iconSrc="@drawable/ic_star"
    app:buttonText="收藏"
    app:textColor="@color/white" />

案例二:封裝 ViewPager + 頂部指示器的組合控件

1. 佈局文件

<!-- res/layout/view_pager_indicator.xml -->
<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:background="@color/gray_bg">

    <!-- 指示器欄 -->
    <LinearLayout
        android:id="@+id/ll_tab_container"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:gravity="center">
        <!-- 動態添加 TextView + View(底線) -->
    </LinearLayout>

    <androidx.viewpager.widget.ViewPager
        android:id="@+id/vp_content"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />
</LinearLayout>

2. 核心邏輯


public class TabViewPagerIndicator extends LinearLayout {
    private LinearLayout llTabContainer;
    private ViewPager viewPager;
    private List tabTexts = new ArrayList<>();
    private List tabIndicators = new ArrayList<>();
    private OnPageChangeListener externalListener;

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

    private void init(Context context) {
        LayoutInflater.from(context).inflate(R.layout.view_pager_indicator, this, true);
        llTabContainer = findViewById(R.id.ll_tab_container);
        viewPager = findViewById(R.id.vp_content);
        viewPager.addOnPageChangeListener(new InternalPageChangeListener());
    }

    /** 設置標題並綁定 Fragment */
    public void setup(List fragments, String[] titles, FragmentManager fm) {
        if (fragments.size() != titles.length) throw new IllegalArgumentException();
        
        llTabContainer.removeAllViews();
        tabTexts.clear();
        tabIndicators.clear();

        for (int i = 0; i < titles.length; i++) {
            // 動態創建 Tab View:TextView + 底線 View
            View tabView = LayoutInflater.from(getContext()).inflate(R.layout.item_tab, llTabContainer, false);
            TextView tv = tabView.findViewById(R.id.tv_title);
            View indicator = tabView.findViewById(R.id.v_indicator);
            tv.setText(titles[i]);
            tv.setTag(i);
            tv.setOnClickListener(v -> viewPager.setCurrentItem((int) v.getTag()));
            
            llTabContainer.addView(tabView, new LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f));
            tabTexts.add(tv);
            tabIndicators.add(indicator);
        }

        // 設置適配器
        viewPager.setAdapter(new FragmentStateAdapter(fm, getLifecycle()) {
            @NonNull @Override public Fragment createFragment(int position) { return fragments.get(position); }
            @Override public int getItemCount() { return fragments.size(); }
        });
        viewPager.setOffscreenPageLimit(fragments.size() - 1);
        updateTabState(0); // 默認第一個
    }

    private void updateTabState(int position) {
        for (int i = 0; i < tabTexts.size(); i++) {
            boolean selected = (i == position);
            tabTexts[i].setTextColor(selected ? 0xFFFF0000 : 0xFF333333); // 紅/黑
            tabIndicators[i].setVisibility(selected ? VISIBLE : INVISIBLE);
        }
    }

    private class InternalPageChangeListener implements ViewPager.OnPageChangeListener {
        @Override public void onPageSelected(int position) {
            updateTabState(position);
            if (externalListener != null) externalListener.onPageSelected(position);
        }
        @Override public void onPageScrollStateChanged(int state) {
            if (externalListener != null) externalListener.onPageScrollStateChanged(state);
        }
        @Override public void onPageScrolled(int p, float o, int oPx) {}
    }

    public void setOnPageChangeListener(OnPageChangeListener l) { this.externalListener = l; }
    public ViewPager getViewPager() { return viewPager; }
}

3. item_tab.xml

<LinearLayout xmlns:android="https://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="16dp"
    android:gravity="center">
    <TextView android:id="@+id/tv_title" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="16sp"/>
    <View android:id="@+id/v_indicator" android:layout_width="24dp" android:layout_height="3dp" android:background="@color/red" android:layout_marginTop="4dp" android:visibility="invisible"/>
</LinearLayout>

4. Activity 中使用


public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        TabViewPagerIndicator tabPager = findViewById(R.id.tab_pager);
        List fragments = Arrays.asList(
            new Fragment1(), new Fragment2(), new Fragment3(), new Fragment4()
        );
        String[] titles = getResources().getStringArray(R.array.tab_titles);
        tabPager.setup(fragments, titles, getSupportFragmentManager());
    }
}

關鍵技術點總結

技術點說明
LayoutInflater.inflate(..., parent, true)將佈局直接附加到當前 ViewGroup (this),返回根 View
TypedArray obtainStyledAttributes解析 XML 中 app:xxx 自定義屬性,用完必須 recycle()
format="reference"屬性值為 @drawable/xxx 或 @color/xxx 等資源引用
動態生成子 ViewLayoutInflater.inflate(item_layout, container, false) + container.addView()
FragmentStateAdapterViewPager2 適配器,自動管理 Fragment 生命週期,替換舊 FragmentPagerAdapter
回調接口解耦定義 OnPageChangeListener 接口,外部監聽不依賴具體實現類

避坑指南

  1. 構造函數:至少實現雙參構造函數 (Context, AttributeSet),三參用於樣式支持。
  2. 屬性命名:declare-styleable 名稱建議與類名一致,屬性名駝峰 iconSrc,R 文件自動生成 R.styleable.ClassName_iconSrc。
  3. measure/layout:組合控件通常不需重寫,除非需特殊測量(如百分比佈局)。
  4. 內存洩漏:持有 Context 使用 ApplicationContext 或 WeakReference;回調接口在 onDetachedFromWindow 置空。
  5. 預覽圖:AS Layout Editor 可識別組合控件,若不顯示可添加 @SuppressLint("CustomViewStyleable") 或重建項目。

總結

組合控件是 Android UI 復用的核心手段:FrameLayout 疊加實現圖文混排、LinearLayout 嵌套封裝指示器+ViewPager。掌握 inflate、自定義屬性、動態添加子 View、Fragment 適配器整合,即可構建高復用、易維護的 UI 組件庫。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved