GsFilter là demo filter ảnh Android nhỏ, đồng thời là module thư viện filter có thể tái sử dụng.
Phiên bản thư viện hiện tại: 1.0.4 trên JitPack.
Dự án gồm:
:filter: module tái sử dụng cho model filter, preview OpenGL, control Filter/Beauty/Adjust, CPU/GPU bitmap render, batch render progress, và thumbnail qua Glide.:app: app mẫu dùng MVVM, ảnh từassets, và module:filter.
Phạm vi hiện tại:
- Min SDK 24.
- Compile SDK 36; app mẫu target SDK 36.
- Gradle wrapper 9.0.0, Android Gradle Plugin 8.13.2, Kotlin 2.2.21, KSP 2.2.21-2.0.4, Java 17.
- Kotlin + XML views.
- App mẫu có camera mode native Camera2: preview gốc và preview filter chạy realtime; nhận diện khuôn mặt chạy bất đồng bộ qua luồng phân tích YUV.
- Preview GPU qua
FilterPreviewView. - Filter preset là data recipe, gồm color/effect/LUT.
- Adjust controls là bộ cố định.
- Beauty controls nằm ở tab riêng, gồm Smoothing, Whitening, Blush, Lipstick, Under-eye, Teeth Whitening, Eye Shadow, Eyeliner, Eyebrow, Face Slimming và Eye Enlargement; tất cả dùng range
0..100. - Thumbnail rail của filter được load bằng Glide với cache key ổn định.
- LUT nội bộ dùng texture 33x33x33 sinh từ
FilterLut, không cần ship file LUT ngoài. - Render nhiều bitmap nên đi qua
FilterRenderer.renderBatch()để xử lý lần lượt và nhận progress %.
Ảnh demo được tải từ app/src/main/assets. Nút Next image chuyển lần lượt qua các ảnh được hỗ trợ trong thư mục assets.
Camera mode hiện dùng trực tiếp SurfaceTexture cho preview GPU và ImageReader YUV cho face tracking. Pipeline CameraX chuyển từng frame RGBA thành bitmap để đi qua đường preview/export bitmap vẫn là phần mở rộng riêng, chưa được bật trong app mẫu.
Thêm JitPack vào settings.gradle.kts của app:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven("https://jitpack.io")
}
}Thêm GsFilter vào module app:
dependencies {
implementation("com.github.vtabk2:GsFilter:1.0.4")
}Nếu project đang dùng Groovy:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}dependencies {
implementation 'com.github.vtabk2:GsFilter:1.0.4'
}Nếu dùng FilterControlsView và thumbnail rail, host app cần thêm Glide KSP processor để AppGlideModule được generate:
plugins {
id("com.google.devtools.ksp")
}
dependencies {
implementation("com.github.bumptech.glide:glide:5.0.7")
ksp("com.github.bumptech.glide:ksp:5.0.7")
}Khai báo KSP plugin version khớp Kotlin, ví dụ Kotlin 2.2.21 dùng KSP 2.2.21-2.0.4.
Filter rail cần host app đăng ký thumbnail loader của thư viện trong AppGlideModule:
@GlideModule
public final class GsFilterGlideModule extends AppGlideModule {
@Override
public void registerComponents(
@NonNull Context context,
@NonNull Glide glide,
@NonNull Registry registry
) {
registry.append(
FilterThumbnailModel.class,
Bitmap.class,
new FilterThumbnailModelLoader.Factory()
);
}
@Override
public boolean isManifestParsingEnabled() {
return false;
}
}App mẫu dùng module local:
dependencies {
implementation(project(":filter"))
implementation("com.github.bumptech.glide:glide:5.0.7")
ksp("com.github.bumptech.glide:ksp:5.0.7")
}Thêm preview view trong XML:
<com.gsfilter.filter.view.FilterPreviewView
android:id="@+id/filterPreview"
android:layout_width="match_parent"
android:layout_height="match_parent" />Sau đó bind bitmap và trạng thái filter hiện tại:
binding.filterPreview.setSourceBitmap(sourceBitmap)
binding.filterPreview.setFilterState(
selectedFilter.recipe,
adjustments,
makeupFeatures = null, // truyền MakeupFeatures nếu host có face detection
)makeupFeatures là tùy chọn; truyền null nếu host không dùng face detection. setFilterState() bỏ qua params trùng nhau và gom các thay đổi adjust nhanh vào frame kế tiếp.
Nếu host không dùng FilterPreviewView, có thể render bitmap kết quả trực tiếp bằng API public trong module :filter:
val resultBitmap = FilterRenderer.getBitmap(
source = sourceBitmap,
recipe = selectedFilter.recipe,
adjustments = adjustments,
maxWidth = 2048,
maxHeight = 2048,
)maxWidth/maxHeight là tùy chọn, renderer chỉ downscale để giữ aspect ratio và không upscale ảnh nhỏ.
API này thử GPU offscreen trước và tự fallback CPU khi EGL không khả dụng. Với ảnh lớn, gọi nó ngoài main thread:
val resultBitmap = withContext(Dispatchers.Default) {
FilterRenderer.getBitmap(
source = sourceBitmap,
recipe = selectedFilter.recipe,
adjustments = adjustments,
maxWidth = 2048,
maxHeight = 2048,
)
}Khi cần render nhiều ảnh, dùng renderBatch() để module xử lý từng bitmap một lượt. Cách này tránh giữ nhiều bitmap kết quả cùng lúc và trả progress sau mỗi ảnh:
withContext(Dispatchers.Default) {
FilterRenderer.renderBatch(
sources = bitmaps,
recipe = selectedFilter.recipe,
adjustments = adjustments,
maxWidth = 2048,
maxHeight = 2048,
onProgress = { progress ->
updateProgress(progress.percent)
},
) { _, bitmap ->
saveBitmap(bitmap)
bitmap.recycle()
}
}FilterRenderProgress.percent trả 0..100; batch rỗng được xem là 100. Callback chạy trên thread gọi renderBatch(), nên chỉ cập nhật UI trực tiếp sau khi chuyển về main thread. Nếu saveBitmap() ghi file blocking, host nên tự chuyển phần lưu sang Dispatchers.IO.
Host app vẫn chịu trách nhiệm decode URI, lưu file, lỗi từng ảnh, cancel job, và permission/MediaStore.
Trong app mẫu, FilterViewModel.renderFilteredBitmap(maxWidth, maxHeight, useGpu) đã bọc sẵn việc chạy background.
FilterControlsView tự render UI category/filter và các tab Beauty/Adjust. Host vẫn giữ app state, preview rendering, save/export, và navigation.
<com.gsfilter.filter.view.FilterControlsView
android:id="@+id/filterControls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:gsFilterCloseIcon="@drawable/ic_gs_tick"
app:gsFilterNoneIcon="@drawable/ic_gs_filter_none"
app:gsFilterBackground="@android:color/transparent"
app:gsFilterContentBackground="@color/your_panel_color"
app:gsFilterCompactTabs="true"
app:gsFilterCompactBackground="@android:color/transparent"
app:gsFilterShowHeader="false"
app:gsFilterShowCloseButton="false"
app:gsFilterShowPopular="true"
app:gsFilterShowBeauty="false"
app:gsFilterBeautyTextColor="@color/your_secondary_color"
app:gsAdjustSecondaryTextColor="@color/your_secondary_color"
app:gsFilterShowTabIndicator="true"
app:gsFilterTabIndicatorColor="@color/your_selected_color"
app:gsFilterTabIndicatorWidthMode="text"
app:gsFilterTabSpacing="20dp" />Nếu tạo view bằng Kotlin, có thể cấu hình tương đương mà không cần XML:
binding.filterControls.setShowHeader(false)
binding.filterControls.setBackgroundColor(Color.TRANSPARENT)Wire callback từ host:
binding.filterControls.onCloseClick = { finish() }
binding.filterControls.onOriginalClick = { /* Host xử lý Original */ }
binding.filterControls.onControlTabSelected = { tab ->
selectedControlTab = tab
renderControlTabs()
}
binding.filterControls.onCategorySelected = { category -> viewModel.selectCategory(category) }
binding.filterControls.onCategoryAutoSelected = { category -> viewModel.autoSelectCategory(category) }
binding.filterControls.onFilterSelected = { filter -> viewModel.selectFilter(filter) }
binding.filterControls.onFilterIntensityChanged = { value -> viewModel.setFilterIntensity(value) }
binding.filterControls.onOriginalFilterPressedChanged = { pressed ->
// Host tự hiển thị Original khi pressed == true và khôi phục khi false.
}
binding.filterControls.onAdjustmentChanged = { control, value -> viewModel.setAdjustment(control, value) }
binding.filterControls.onResetAllAdjustClick = { viewModel.resetAdjustments() }Render state hiện tại ngược lại vào view:
binding.filterControls.setState(
selectedCategory = state.selectedCategory,
selectedFilter = state.selectedFilter,
thumbnailBitmap = state.sourceBitmap,
thumbnailKey = state.filterThumbnailKey,
selectedRecipe = state.selectedRecipe,
)
binding.filterControls.setAdjustments(state.adjustments)Nếu host không có override intensity theo filter, có thể dùng overload setState() không truyền selectedRecipe.
FilterControlsView mặc định dùng FilterCatalog.pack. Host có thể thay bằng catalog riêng qua setCatalog(FilterPack(...)); sau đó dùng cùng các callback và setState() như trên.
Ghi chú:
Originallà action cố định ở đầu, dùng none icon.- Khi
gsFilterShowHeader="false", hàng compact phía trên nội dung luôn hiển thị; toàn bộ seek row của tab đang chọn (Filter/Beauty/Adjust), gồm reset từng control, seekbar và value, được đưa lên hàng này để thống nhất UI. Chỉ nútReset Allvà danh sách control giữ ở bên dưới; Filter vẫn giữ category cùng nút None bên dưới. Icon Original compact là nút riêng, chỉ hiện khi Filter/Beauty/Adjust có thay đổi và làINVISIBLEkhi trạng thái sạch. Reset Filter dùngonFilterIntensityChanged, còn icon Original chỉ phátonOriginalClickhoặconOriginalFilterPressedChanged(true/false)để host tự xử lý. - Khi Popular hiển thị, đổi category sẽ đổi danh sách filter đang hiển thị. Khi Popular không hiển thị, rail thumbnail chứa toàn bộ filter (trừ Original) và tự cập nhật category theo filter đang thấy khi người dùng scroll.
- Filter chỉ được áp dụng sau khi người dùng bấm vào filter item. Bấm category chỉ scroll rail tới filter đầu tiên của category đó.
- Bấm trực tiếp filter chỉ cập nhật lựa chọn và giữ vị trí rail hiện tại; rail chỉ tự scroll khi đổi category hoặc đổi ảnh.
- Khi Popular không hiển thị, category tự cập nhật theo vị trí scroll; filter đang chọn chỉ đổi khi người dùng bấm filter.
- Tab Adjust được render trong cùng
FilterControlsView; host nhận callback adjust và gửiAdjustmentshiện tại lại bằngsetAdjustments(). - Trong tab Adjust, hàng icon control nằm trên seekbar/value hiện tại; khi header ẩn, seekbar row (gồm reset control và value) chuyển lên compact row, còn Reset All vẫn ở dưới danh sách control. Các nút reset dùng trạng thái disabled khi không có gì để reset.
gsFilterCompactTabskéo label/indicator của tab Filter và Adjust gần nhau hơn;gsFilterTabSpacingchỉnh khoảng cách giữa hai tab.gsFilterCloseIconđổi icon củags_filter_close_button; app mẫu đang dùngic_gs_tick.gsFilterShowCloseButtonẩn riêng nút đóng bằngINVISIBLE; mặc địnhtrue.gsAdjustResetIconmặc định dùng selector có disabled/pressed state.
Các màu mặc định của module được tách trong filter/src/main/res/values/filter_colors.xml và
filter/src/main/res/values-night/filter_colors.xml. Host app có thể truyền màu riêng qua XML
attributes bằng các resource tương ứng trong values/ và values-night/.
Để chỉ làm hàng seekbar compact trong suốt nhưng giữ cụm category/thumbnail có nền:
app:gsFilterBackground="@android:color/transparent"
app:gsFilterCompactBackground="@android:color/transparent"
app:gsFilterContentBackground="@color/your_panel_color"App mẫu có values-night và Theme.GsFilter tương ứng để đi theo dark mode của thiết bị.
Beauty là tab chỉnh khuôn mặt và makeup riêng, tách khỏi Adjust. Người dùng chọn một control trên thanh icon ngang rồi kéo SeekBar chung ở dưới; tất cả control dùng range 0..100.
Các control hiện có:
Smoothing: làm mịn da, có bảo vệ cạnh để giữ chi tiết khuôn mặt.Whitening: nâng sáng và giảm nhẹ độ bão hòa vùng da, không phủ trắng toàn ảnh.Blush: thêm má hồng theo hai vùng má được phát hiện, có hỗ trợ mặt nghiêng.Lipstick: tô môi theo contour môi, giới hạn màu trong vùng môi.Under-eye: làm sáng vùng dưới mắt.Teeth Whitening: làm sáng vùng răng trong khu vực miệng.Eye Shadow: thêm màu mắt theo vùng mí và góc mặt.Eyeliner: thêm đường viền mắt theo vùng mắt.Eyebrow: tăng màu theo contour lông mày.Face Slimming: thu gọn khuôn mặt bằng warp giới hạn trong vùng mặt.Eye Enlargement: phóng to mắt bằng warp theo vùng mắt và góc nghiêng.
Beauty state được giữ trong FilterRecipe, render bằng GPU và có CPU fallback. Nút Reset Beauty đưa control đang chọn về giá trị của filter hiện tại; Reset All Beauty đưa toàn bộ Beauty về giá trị mặc định của filter. Khi đang ở Original, các giá trị mặc định đều là 0.
Khi gsFilterShowHeader="false", seekbar row của Beauty (reset control, seekbar và value) được hiển thị trong gs_filter_compact_controls; danh sách control và Reset All Beauty vẫn nằm bên dưới.
Host app nhận thay đổi Beauty qua callback:
binding.filterControls.onBeautyChanged = { control, value ->
when (control) {
FilterControlsView.BeautyControl.Smoothing -> viewModel.setSkinSmoothing(value)
FilterControlsView.BeautyControl.Whitening -> viewModel.setSkinWhitening(value)
FilterControlsView.BeautyControl.Blush -> viewModel.setBlush(value)
FilterControlsView.BeautyControl.Lipstick -> viewModel.setLipstick(value)
FilterControlsView.BeautyControl.UnderEye -> viewModel.setUnderEye(value)
FilterControlsView.BeautyControl.TeethWhitening -> viewModel.setTeethWhitening(value)
FilterControlsView.BeautyControl.EyeShadow -> viewModel.setEyeShadow(value)
FilterControlsView.BeautyControl.Eyeliner -> viewModel.setEyeliner(value)
FilterControlsView.BeautyControl.Eyebrow -> viewModel.setEyebrow(value)
FilterControlsView.BeautyControl.FaceSlimming -> viewModel.setFaceSlimming(value)
FilterControlsView.BeautyControl.EyeEnlargement -> viewModel.setEyeEnlargement(value)
}
}
binding.filterControls.onResetBeautyClick = {
viewModel.resetBeauty()
}Host có thể map từng BeautyControl vào state riêng như app mẫu.
Built-in FilterCatalog có 103 preset, không tính action Original. Một preset có thể xuất hiện ở nhiều category; Popular là nhóm shortcut cho các mẫu hay dùng.
| Category | Preset hỗ trợ |
|---|---|
| Popular | Fresh, Warm, Tasty, B&W, Clear, Gold, Teal Orange, Cream, Glow, Sunset, Blockbuster, Soft, Beauty, Golden Hour, Travel, Neon, Clean Light, Selfie Clear, Fresh Plate, Clean Portrait, Daylight Fresh, Food Pop, Teal Cinema |
| Portrait | Portra, Skin, Peach, Cream, Glow, Soft, Blush, Rosy, Tan, Beauty, Soft Mono, Selfie Clear, Soft Portrait, Studio Skin, Golden Skin, Indoor Warm, Flash Soft, Low Light Skin, Pearl Mono, Clean Portrait, Soft Skin, Golden Portrait |
| Natural | Fresh, Clear, Airy, Pure, Clean Light, Soft Day, Daylight Fresh |
| Food | Tasty, Crispy, Cafe, Warm Plate, Fresh Plate, Warm Table, Food Pop |
| Landscape | Clear Day, Golden Hour, Winter, Forest, Ocean, Sky, Travel, Blue Hour, Sunlit Forest, Green Film |
| Night | Neon, City, Night, Blue Hour, Cyberpunk, Low Light Skin, Midnight City, Night Mood |
| Film | Portra, Fuji, Gold, Kodak, Grain Film |
| Cinematic | Cinema, Teal Orange, Noir, Drama, Epic, Blockbuster, Arthouse, Bleach, Deep Teal, Fade Drama, Teal Cinema |
| Vintage | Retro, Fade, Dust, Oldie, 90s, Retro Matte, Vintage Fade |
| B&W | B&W, Noir, Matte, High Contrast, Soft Mono, Pearl Mono |
| Warm | Warm, Sunset, Amber, Caramel, Cozy |
| Cool | Cool, Arctic, Mist, Blue Mist, Steel, Cyan Clean |
| Aesthetic | Beige, Latte, Pink, Dreamy, Minimal, Editorial Matte |
| Creative | Neon, Cyberpunk, Purple, Dream, Fantasy |
| Art | Pencil, Soft Sketch, Color Pencil, Fine Line, Ink, Charcoal |
Filter rail dùng Glide để render và cache thumbnail đã apply filter từ FilterThumbnailModel.
Truyền source bitmap và source key ổn định vào setState():
FilterSourceKey.asset("sample.jpg")
FilterSourceKey.file(path, length, lastModifiedMillis)
FilterSourceKey.uri(uri, width, height, lastModifiedMillis)Dùng lại đúng key khi cùng một ảnh source được chọn lại. Glide có thể lấy lại thumbnail filter từ cache thay vì render lại toàn bộ rail.
Thumbnail đưa vào Glide cache được bound theo request, tối đa 256px cho cả filter màu và filter Art/effect. Cache key gồm source key, kích thước source, filter recipe, adjustments, render version, và kích thước thumbnail bound.
Trong app mẫu, khi không có thay đổi filter, Beauty hoặc Adjust, đường export dùng bản sao pixel-identical của bitmap gốc thay vì chạy bitmap qua GPU shader. Để kiểm tra lúc bấm lưu, xem log tag BitmapCompare trong Logcat.
Các XML attributes hiện có:
| Attribute | Mục đích |
|---|---|
gsFilterTextColor |
Màu category/icon thường và fallback cho text tab |
gsFilterSelectedTextColor |
Màu category/icon đang chọn và fallback cho selected tab khi tab background bật |
gsFilterTabTextColor |
Màu text tab Filter/Adjust bình thường; mặc định theo gsFilterTextColor |
gsFilterSelectedTabTextColor |
Màu text tab Filter/Adjust đang chọn; mặc định theo gsFilterSelectedTextColor, hoặc gsFilterSelectedColor khi tab background tắt |
gsFilterSelectedColor |
Accent fallback cho trạng thái selected |
gsFilterChipBackground |
Background category chip bình thường |
gsFilterSelectedChipBackground |
Background category chip đang chọn |
gsFilterTabBackground |
Background tab Filter/Adjust bình thường; mặc định theo gsFilterChipBackground |
gsFilterSelectedTabBackground |
Background tab Filter/Adjust đang chọn; mặc định theo gsFilterSelectedChipBackground |
gsFilterUseTabBackground |
Set false để render tab Filter/Adjust không có background |
gsFilterCardBackground |
Background filter thumbnail card bình thường |
gsFilterSelectedCardBackground |
Background filter thumbnail card đang chọn |
gsFilterCardForeground |
Foreground viền thumbnail card bình thường, nằm trên ảnh |
gsFilterSelectedCardForeground |
Foreground viền thumbnail card đang chọn, nằm trên ảnh |
gsFilterLabelBackground |
Background nhãn thumbnail |
gsFilterLabelTextColor |
Màu text nhãn thumbnail |
gsFilterCloseIcon |
Drawable icon đóng/xác nhận; app mẫu có ic_gs_tick |
gsFilterNoneIcon |
Drawable icon Original/none |
gsFilterIconPadding |
Override padding icon close/original nếu cần; bỏ trống thì dùng default của RippleImageView |
gsFilterShowTabIndicator |
Hiển thị indicator dưới tab Filter/Adjust đang chọn |
gsFilterCompactTabs |
Kéo label/indicator của tab Filter và Adjust gần nhau hơn trong khi vùng bấm vẫn rộng |
gsFilterShowHeader |
Hiển thị/ẩn toàn bộ header gs_filter_header; mặc định true |
gsFilterShowCloseButton |
Hiển thị/ẩn riêng nút đóng; dùng INVISIBLE khi false để giữ cân layout; mặc định true |
gsFilterBackground |
Background của toàn bộ FilterControlsView; mặc định @color/gs_panel_background |
gsFilterContentBackground |
Background riêng cho cụm category/thumbnail gs_filter_content; mặc định không ghi đè |
gsFilterHeaderBackground |
Background riêng của header; có thể đặt @android:color/transparent |
gsFilterCompactBackground |
Background riêng của gs_filter_compact_controls; có thể đặt @android:color/transparent |
gsFilterShowBeauty |
Hiển thị/ẩn tab và nội dung Beauty; mặc định true |
gsFilterBeautyTextColor |
Màu icon/label Beauty chưa chọn và text Reset All Beauty khi disabled; enabled dùng gsFilterIntensityProgressColor |
gsFilterTabSpacing |
Khoảng cách giữa tab Filter và Adjust khi cần chỉnh gần/xa nhau |
gsFilterTabIndicatorColor |
Màu tab indicator |
gsFilterTabIndicatorHeight |
Chiều cao tab indicator |
gsFilterTabIndicatorWidthMode |
Kích thước ngang indicator: full, min, hoặc text |
gsFilterTabIndicatorMinWidth |
Chiều rộng indicator khi mode là min; mặc định theo gs_filter_chip_min_width |
gsFilterShowIntensity |
Bật/tắt slider Intensity cho filter có recipe |
gsFilterIntensityTextColor |
Màu label và value của slider Intensity |
gsFilterIntensityProgressColor |
Màu progress và thumb của slider Intensity |
gsFilterIntensityTrackColor |
Màu track của slider Intensity |
gsAdjustTextColor |
Màu text giá trị adjust |
gsAdjustSecondaryTextColor |
Màu icon/label adjust item chưa chọn |
gsAdjustSelectedColor |
Màu adjust item đang chọn, seekbar progress/thumb, và changed-dot |
gsAdjustTrackColor |
Màu track của adjust seekbar |
gsAdjustResetIcon |
Drawable icon reset control adjust hiện tại |
gsAdjustResetIconPadding |
Override padding icon reset nếu cần; bỏ trống thì dùng default của RippleImageView |
gsAdjustResetAllText |
Text nút reset tất cả |
Các dimension có thể override:
| Dimension | Default | Mục đích |
|---|---|---|
gs_filter_item_spacing |
8dp |
Khoảng cách mặc định giữa các filter control |
gs_filter_chip_min_width |
80dp |
Chiều rộng tối thiểu của category chip và fallback indicator min |
gs_filter_chip_min_height |
32dp |
Chiều cao tối thiểu của tab và category chip |
gs_filter_chip_horizontal_padding |
18dp |
Padding ngang của tab và category chip |
gs_filter_chip_vertical_padding |
4dp |
Padding dọc của tab và category chip |
gs_filter_category_top_spacing |
20dp |
Khoảng cách trên giữa tab row Filter/Adjust và category row |
gs_filter_tab_indicator_height |
2dp |
Chiều cao indicator của tab Filter/Adjust |
gs_filter_thumbnail_width |
78dp |
Chiều rộng item thumbnail filter |
gs_filter_thumbnail_height |
102dp |
Chiều cao item thumbnail filter |
gs_filter_thumbnail_label_height |
24dp |
Chiều cao dải label thumbnail filter |
gs_filter_thumbnail_label_padding |
4dp |
Padding ngang label thumbnail filter |
gs_adjust_icon_size |
24dp |
Kích thước icon adjust control |
gs_adjust_dot_size |
5dp |
Kích thước dot báo value đã đổi |
gs_adjust_item_gap |
4dp |
Khoảng cách giữa dot, icon, và label của adjust item |
gs_adjust_item_width |
78dp |
Chiều rộng adjust item trong horizontal rail |
gs_adjust_value_width |
36dp |
Chiều rộng text giá trị adjust hiện tại |
Host app có thể override dimension của thư viện bằng cách khai báo cùng resource name trong values/dimens.xml của app:
<dimen name="gs_filter_category_top_spacing">20dp</dimen>Với thay đổi visual lớn hơn, ưu tiên thay drawable qua các attr ở trên trước khi thêm custom layout API mới.
Hướng mở rộng nên đi theo data trước:
- Thêm hoặc load thêm
FilterOptionrecipes. - Group chúng bằng
FilterCategory. - Giữ user
Adjustmentstách riêng; recipe adjustments và user adjustments được cộng lại khi render. - Dùng
lut/lutStrengthkhi recipe thường chưa đủ; chỉ thêm shader mới khi cả recipe và LUT hiện có không diễn tả được look mong muốn.
Cách này giúp thư viện filter dễ mở rộng hơn so với mỗi filter một class riêng hoặc phụ thuộc cứng vào GPUImage internals.