این راهنما نمونهای را طی میکند که برخی از بهترین روشها را برای پشتیبانی از دسترسی با نماهای سفارشی در برنامه Android TV نشان میدهد. راهنما نحوه ایجاد یک فعالیت ساده را نشان میدهد که از Canvas API برای ترسیم چهار تصویر قابل فوکوس استفاده میکند و به سرویسهای دسترسپذیری اجازه میدهد بین آنها حرکت کنند.
فایل XML طرح بندی ایجاد کنید
یک فایل طرح بندی برای فعالیت نمای سفارشی ایجاد کنید.
<?xml version="1.0" encoding="utf-8"> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content"> <LinearLayout xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".custom.CustomViewActivity"> <com.example.tvcustomviews.SampleCustomView android:layout_width="match_parent" android:layout_height="wrap_content" /> </LinearLayout> </LinearLayout>
بخش های زیر کلاس com.example.tvcustomviews.SampleCustomView
را تعریف می کنند.
یک فعالیت ساده را تعریف کنید
یک فعالیت ساده برای بارگیری فایل XML طرح بندی ایجاد کنید.
کاتلین
import androidx.appcompat.app.AppCompatActivity import android.os.Bundle class CustomViewBestPracticeActivity:AppCompatActivity() { protected fun onCreate(savedInstanceState:Bundle) { super.onCreate(savedInstanceState) setContentView(R.layout.custom_view_best_practices) } }
جاوا
package com.example.tvcustomviews; import androidx.appcompat.app.AppCompatActivity; import android.os.Bundle; public class CustomViewBestPracticeActivity extends AppCompatActivity{ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.custom_view_best_practices); } }
کلاس SampleCustomView را تعریف کنید
بعد، کلاس را تعریف کنید و با استفاده از یک بوم چهار تصویر بکشید.
کاتلین
package com.example.tvcustomviews import android.content.Context import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import android.graphics.Rect import android.util.AttributeSet import android.view.View import androidx.annotation.Nullable import androidx.core.view.ViewCompat import java.util.HashMap class SampleCustomView:View { internal val virtualIdRectMap:Map<Int, VirtualRect> = HashMap<Int, VirtualRect>() private val rectangleId = 1 internal class VirtualRect(rect:Rect, paint:Paint, id:Int) { val rect:Rect val paint:Paint val id:Int = 0 init{ this.rect = rect this.paint = paint this.id = id } } constructor(context:Context) : super(context) { init() } constructor(context:Context, @Nullable attrs:AttributeSet) : super(context, attrs) { init() } constructor(context:Context, @Nullable attrs:AttributeSet, defStyleAttr:Int) : super(context, attrs, defStyleAttr) { init() } private fun init() { setRectangle() ViewCompat.setAccessibilityDelegate(this, SampleExploreByTouchHelper(this)) } protected fun onDraw(canvas:Canvas) { super.onDraw(canvas) for (mapElement in virtualIdRectMap.entries) { val virtualRect = mapElement.value canvas.drawRect(virtualRect.rect, virtualRect.paint) } } private fun setRectangle() { val paint = Paint(Paint.ANTI_ALIAS_FLAG) paint.setColor(RECTANGLE_COLOR) val leftTopRectangle = Rect() leftTopRectangle.left = RECTANGLE_START_POINT_LEFT leftTopRectangle.right = leftTopRectangle.left + RECTANGLE_WIDTH leftTopRectangle.top = RECTANGLE_START_POINT_TOP leftTopRectangle.bottom = leftTopRectangle.top + RECTANGLE_HEIGHT virtualIdRectMap.put(rectangleId, VirtualRect(leftTopRectangle, paint, rectangleId++)) val rightTopRectangle = Rect() rightTopRectangle.left = leftTopRectangle.right + HORIZONTAL_PADDING rightTopRectangle.right = rightTopRectangle.left + RECTANGLE_WIDTH rightTopRectangle.top = leftTopRectangle.top rightTopRectangle.bottom = rightTopRectangle.top + RECTANGLE_HEIGHT virtualIdRectMap.put(rectangleId, VirtualRect(rightTopRectangle, paint, rectangleId++)) val leftBottomRectangle = Rect() leftBottomRectangle.left = leftTopRectangle.left leftBottomRectangle.right = leftBottomRectangle.left + RECTANGLE_WIDTH leftBottomRectangle.top = leftTopRectangle.bottom + VERTICAL_PADDING leftBottomRectangle.bottom = leftBottomRectangle.top + RECTANGLE_HEIGHT virtualIdRectMap.put(rectangleId, VirtualRect(leftBottomRectangle, paint, rectangleId++)) val rightBottomRectangle = Rect() rightBottomRectangle.left = leftBottomRectangle.right + HORIZONTAL_PADDING rightBottomRectangle.right = rightBottomRectangle.left + RECTANGLE_WIDTH rightBottomRectangle.top = leftBottomRectangle.top rightBottomRectangle.bottom = rightBottomRectangle.top + RECTANGLE_HEIGHT virtualIdRectMap.put(rectangleId, VirtualRect(rightBottomRectangle, paint, rectangleId++)) } companion object { private val RECTANGLE_START_POINT_LEFT = 50 private val RECTANGLE_START_POINT_TOP = 100 private val RECTANGLE_WIDTH = 200 private val RECTANGLE_HEIGHT = 100 private val HORIZONTAL_PADDING = 50 private val VERTICAL_PADDING = 100 private val RECTANGLE_COLOR = Color.argb(100, 0, 163, 245) } }
جاوا
package com.example.tvcustomviews; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Rect; import android.util.AttributeSet; import android.view.View; import androidx.annotation.Nullable; import androidx.core.view.ViewCompat; import java.util.HashMap; import java.util.Map; public class SampleCustomView extends View { final Map<Integer, VirtualRect> virtualIdRectMap = new HashMap<>(); private static final int RECTANGLE_START_POINT_LEFT = 50; private static final int RECTANGLE_START_POINT_TOP = 100; private static final int RECTANGLE_WIDTH = 200; private static final int RECTANGLE_HEIGHT = 100; private static final int HORIZONTAL_PADDING = 50; private static final int VERTICAL_PADDING = 100; private static final int RECTANGLE_COLOR = Color.argb(100, 0, 163, 245); private int rectangleId = 1; static class VirtualRect { final Rect rect; final Paint paint; final int id; VirtualRect(Rect rect, Paint paint, int id) { this.rect = rect; this.paint = paint; this.id = id; } } public SampleCustomView(Context context) { super(context); init(); } public SampleCustomView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); init(); } public SampleCustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(); } private void init() { setRectangle(); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); for (Map.Entry<Integer, VirtualRect> mapElement : virtualIdRectMap.entrySet()) { VirtualRect virtualRect = mapElement.getValue(); canvas.drawRect(virtualRect.rect, virtualRect.paint); } } private void setRectangle() { Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); paint.setColor(RECTANGLE_COLOR); Rect leftTopRectangle = new Rect(); leftTopRectangle.left = RECTANGLE_START_POINT_LEFT; leftTopRectangle.right = leftTopRectangle.left + RECTANGLE_WIDTH; leftTopRectangle.top = RECTANGLE_START_POINT_TOP; leftTopRectangle.bottom = leftTopRectangle.top + RECTANGLE_HEIGHT; virtualIdRectMap.put(rectangleId, new VirtualRect(leftTopRectangle, paint, rectangleId++)); Rect rightTopRectangle = new Rect(); rightTopRectangle.left = leftTopRectangle.right + HORIZONTAL_PADDING; rightTopRectangle.right = rightTopRectangle.left + RECTANGLE_WIDTH; rightTopRectangle.top = leftTopRectangle.top; rightTopRectangle.bottom = rightTopRectangle.top + RECTANGLE_HEIGHT; virtualIdRectMap.put(rectangleId, new VirtualRect(rightTopRectangle, paint, rectangleId++)); Rect leftBottomRectangle = new Rect(); leftBottomRectangle.left = leftTopRectangle.left; leftBottomRectangle.right = leftBottomRectangle.left + RECTANGLE_WIDTH; leftBottomRectangle.top = leftTopRectangle.bottom + VERTICAL_PADDING; leftBottomRectangle.bottom = leftBottomRectangle.top + RECTANGLE_HEIGHT; virtualIdRectMap.put(rectangleId, new VirtualRect(leftBottomRectangle, paint, rectangleId++)); Rect rightBottomRectangle = new Rect(); rightBottomRectangle.left = leftBottomRectangle.right + HORIZONTAL_PADDING; rightBottomRectangle.right = rightBottomRectangle.left + RECTANGLE_WIDTH; rightBottomRectangle.top = leftBottomRectangle.top; rightBottomRectangle.bottom = rightBottomRectangle.top + RECTANGLE_HEIGHT; virtualIdRectMap.put(rectangleId, new VirtualRect(rightBottomRectangle, paint, rectangleId++)); } }
برنامه در این حالت هنگام اجرا چهار مستطیل آبی را روی صفحه نمایش می دهد. اما وقتی Talkback روشن است، نمی توانید به مستطیل ها بروید. به عبارت دیگر، این مستطیل ها نمی توانند فوکوس دسترسی داشته باشند.
ExploreByTouchHelper را برای افشای AccessibilityNodeInfo تعریف کنید
SampleExploreByTouchHelper
را با به ارث بردن ExploreByTouchHelper
و پیاده سازی متدهای آن تعریف کنید.
کاتلین
package com.example.tvcustomviews import android.graphics.Rect import android.os.Bundle import android.util.Log import android.widget.ImageView import androidx.annotation.NonNull import androidx.annotation.Nullable import androidx.core.view.accessibility.AccessibilityNodeInfoCompat import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat import androidx.customview.widget.ExploreByTouchHelper import com.example.tvcustomviews.SampleCustomView.VirtualRect import java.util.Objects internal class SampleExploreByTouchHelper(parentView:SampleCustomView):ExploreByTouchHelper(parentView) { private val TAG = SampleExploreByTouchHelper::class.java!!.getName() private val parentView:SampleCustomView init{ this.parentView = parentView } /** * Define AccessibilityNodeInfo objects with necessary attributes. * Set required attributes into the AccessibilityNodeInfo for the UI * component whose virtualID is the virtualViewId. * @param virtualViewId: virtual ID for the component. * @param node: the AccessibilityNodeInfoCompat used to set the attribute. */ protected fun onPopulateNodeForVirtualView( virtualViewId:Int, @NonNull node:AccessibilityNodeInfoCompat) { val rectEle = parentView.virtualIdRectMap.get(virtualViewId) assert(rectEle != null) node.setContentDescription("Selected rect " + rectEle.id) node.setClassName(ImageView::class.java!!.getName()) node.setPackageName(BuildConfig.APPLICATION_ID) node.setVisibleToUser(true) node.addAction(AccessibilityActionCompat.ACTION_CLICK) // ***************************************************************** // It is very important to set the rectangle area for each node so // the accessibility service knows where to set accessibility focus. // ***************************************************************** node.setBoundsInParent( Objects.requireNonNull(parentView.virtualIdRectMap.get( virtualViewId)).rect) } protected fun getVirtualViewAt(x:Float, y:Float):Int { for (mapElement in parentView.virtualIdRectMap.entrySet()) { val rect = mapElement.value.rect if ((rect.left <= x && x <= rect.right && rect.top <= y && y <= rect.bottom)) { return mapElement.value.id } } return ExploreByTouchHelper.INVALID_ID } protected fun getVisibleVirtualViews(virtualViewIds:List<Int>) { for (mapElement in parentView.virtualIdRectMap.entrySet()) { virtualViewIds.add(mapElement.key) } } protected fun onPerformActionForVirtualView( virtualViewId:Int, action:Int, @Nullable arguments:Bundle):Boolean { when (action) { // After Talkback is turned on, if you want to handle // DPAD_CENTER key event with your own logic, // implement it under ACTION_CLICK action. AccessibilityNodeInfoCompat.ACTION_CLICK -> { Log.e(TAG, "ACTION_CLICK action handled here.") return true } } // All the DPAD directional keys are handled by // Accessibility based on the AccessibilityNodeInfo Tree // defined above. // fall through return false } }
جاوا
package com.example.tvcustomviews; import android.graphics.Rect; import android.os.Bundle; import android.util.Log; import android.widget.ImageView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat; import androidx.customview.widget.ExploreByTouchHelper; import com.example.tvcustomviews.SampleCustomView.VirtualRect; import java.util.List; import java.util.Map; import java.util.Objects; class SampleExploreByTouchHelper extends ExploreByTouchHelper { private final String TAG = SampleExploreByTouchHelper.class.getName(); private SampleCustomView parentView; SampleExploreByTouchHelper(SampleCustomView parentView) { super(parentView); this.parentView = parentView; } /** * Define AccessibilityNodeInfo objects with necessary attributes. * Set required attributes into the AccessibilityNodeInfo for the UI * component whose virtualID is the virtualViewId. * @param virtualViewId: virtual ID for the component. * @param node: the AccessibilityNodeInfoCompat used to set the attribute. */ @Override protected void onPopulateNodeForVirtualView( int virtualViewId, @NonNull AccessibilityNodeInfoCompat node) { VirtualRect rectEle = parentView.virtualIdRectMap.get(virtualViewId); assert rectEle != null; node.setContentDescription("Selected rect " + rectEle.id); node.setClassName(ImageView.class.getName()); node.setPackageName(BuildConfig.APPLICATION_ID); node.setVisibleToUser(true); node.addAction(AccessibilityActionCompat.ACTION_CLICK); // ***************************************************************** // It is very important to set the rectangle area for each node so // the accessibility service knows where to set accessibility focus. // ***************************************************************** node.setBoundsInParent( Objects.requireNonNull(parentView.virtualIdRectMap.get( virtualViewId)).rect); } @Override protected int getVirtualViewAt(float x, float y) { for (Map.Entry<Integer, VirtualRect> mapElement : parentView.virtualIdRectMap.entrySet()) { Rect rect = mapElement.getValue().rect; if (rect.left <= x && x <= rect.right && rect.top <= y && y <= rect.bottom) { return mapElement.getValue().id; } } return ExploreByTouchHelper.INVALID_ID; } @Override protected void getVisibleVirtualViews(List<Integer> virtualViewIds) { for (Map.Entry<Integer, VirtualRect> mapElement : parentView.virtualIdRectMap.entrySet()) { virtualViewIds.add(mapElement.getKey()); } } @Override protected boolean onPerformActionForVirtualView( int virtualViewId, int action, @Nullable Bundle arguments) { switch (action) { // After Talkback is turned on, if you want to handle // DPAD_CENTER key event with your own logic, // implement it under ACTION_CLICK action. case AccessibilityNodeInfoCompat.ACTION_CLICK: Log.e(TAG, "ACTION_CLICK action handled here."); return true; // All the DPAD directional keys are handled by // Accessibility based on the AccessibilityNodeInfo Tree // defined above. default: // fall through } return false; } }
در نهایت، هنگام نمونه سازی نمای سفارشی setAccessibilityDelegate()
را فراخوانی کنید.
کاتلین
private fun init(@Nullable attrs:AttributeSet) { ... ViewCompat.setAccessibilityDelegate(this, SampleExploreByTouchHelper(this)) }
جاوا
private void init(@Nullable AttributeSet attrs) { ... ViewCompat.setAccessibilityDelegate(this, new SampleExploreByTouchHelper(this)); }
با فعال کردن Talkback، فوکوس دسترسپذیری اکنون میتواند به درستی به چهار تصویر داخل نمای سفارشی پیمایش کند. اگر کلید DPAD_CENTER
را روی صفحه کلید فشار دهید، برنامه این رویداد را به عنوان یک رویداد ACTION_CLICK
ثبت میکند.